Преглед изворни кода

Merge branch 'master' of http://39.101.143.165:8090/785834757/jzg

lipf пре 2 месеци
родитељ
комит
398e8a0269

+ 189 - 38
commons/src/main/java/com/jzg/commons/util/ImageUtil.java

@@ -126,10 +126,16 @@ public class ImageUtil {
 
     /**
      * 图片压缩参数配置
+     * - MAX_WIDTH/MAX_HEIGHT: 图片最大尺寸限制,超过则按比例缩放
+     * - INITIAL_QUALITY: 初始压缩质量(0.0-1.0),值越高质量越好但文件越大
+     * - MIN_QUALITY: 最低压缩质量,达到此值后停止迭代
+     * - QUALITY_STEP: 每次迭代降低的质量步长
      */
     private static final int MAX_WIDTH = 1920;
     private static final int MAX_HEIGHT = 1080;
-    private static final float QUALITY = 0.7f;
+    private static final float INITIAL_QUALITY = 0.7f;
+    private static final float MIN_QUALITY = 0.1f;
+    private static final float QUALITY_STEP = 0.1f;
 
     /**
      * 判断文件是否为图片类型(通过 Content-Type 判断)
@@ -158,70 +164,175 @@ public class ImageUtil {
 
     /**
      * 图片压缩方法
+     * 
      * 压缩策略:
-     * 1. 如果图片尺寸超过 MAX_WIDTH x MAX_HEIGHT,先按比例缩放
-     * 2. 无论是否缩放,都进行质量压缩(压缩质量为 QUALITY)
-     * 3. 保持原图片格式,PNG 保持透明通道
+     * 1. 尺寸压缩:如果图片尺寸超过 MAX_WIDTH x MAX_HEIGHT,先按比例缩放至最大尺寸以内
+     * 2. 迭代质量压缩:从 INITIAL_QUALITY(0.7) 开始逐步降低质量,直到压缩后文件小于原图或达到 MIN_QUALITY(0.1)
+     * 3. 格式降级:如果PNG格式压缩无效,自动尝试转为JPEG格式进行有损压缩
+     * 4. 透明通道处理:转为JPEG时,将透明通道填充为白色背景
      *
      * @param file 上传的图片文件
-     * @return 压缩后的字节数组,压缩失败返回 null
+     * @return 压缩后的字节数组,压缩失败或压缩后更大则返回 null(调用方应使用原文件)
      */
     public static byte[] compressImage(MultipartFile file) {
+        // 参数校验:文件为空直接返回null
         if (file == null || file.isEmpty()) {
             return null;
         }
 
+        // 获取原图大小,用于后续压缩效果对比
+        long originalSize = file.getSize();
+
         try (InputStream is = file.getInputStream()) {
+            // 读取图片为BufferedImage对象
             BufferedImage originalImage = ImageIO.read(is);
             if (originalImage == null) {
+                log.error("无法读取图片,ImageIO.read返回null,可能是不支持的图片格式");
                 return null;
             }
 
+            // 获取图片原始尺寸
             int width = originalImage.getWidth();
             int height = originalImage.getHeight();
-
-            // 如果图片尺寸在限制范围内,直接进行质量压缩
-            if (width <= MAX_WIDTH && height <= MAX_HEIGHT) {
-                return compressWithQuality(originalImage, file.getOriginalFilename());
+            log.debug("图片原始尺寸: {} x {}, 文件大小: {} bytes", width, height, originalSize);
+
+            // 默认使用原图,如需缩放则替换
+            BufferedImage targetImage = originalImage;
+
+            // 步骤1:尺寸压缩 - 超过最大尺寸则按比例缩放
+            if (width > MAX_WIDTH || height > MAX_HEIGHT) {
+                // 计算缩放比例,保持宽高比,取最小比例确保不超过任一维度限制
+                double scale = Math.min((double) MAX_WIDTH / width, (double) MAX_HEIGHT / height);
+                int newWidth = (int) (width * scale);
+                int newHeight = (int) (height * scale);
+
+                // 创建缩放后的图片,使用双线性插值保证清晰度
+                targetImage = new BufferedImage(newWidth, newHeight, originalImage.getType());
+                Graphics2D g2d = targetImage.createGraphics();
+                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+                g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
+                g2d.dispose();
+                log.debug("图片缩放后尺寸: {} x {}", newWidth, newHeight);
             }
 
-            // 计算缩放比例,保持宽高比
-            double scale = Math.min((double) MAX_WIDTH / width, (double) MAX_HEIGHT / height);
-            int newWidth = (int) (width * scale);
-            int newHeight = (int) (height * scale);
+            // 获取图片格式(从文件名解析)
+            String format = getImageFormat(file.getOriginalFilename());
+            
+            // 步骤2:迭代质量压缩,尝试不同质量档位
+            byte[] compressedBytes = compressWithIterativeQuality(targetImage, format, originalSize);
 
-            // 创建缩放后的图片,使用双线性插值算法保证清晰度
-            BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, originalImage.getType());
-            Graphics2D g2d = resizedImage.createGraphics();
-            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
-            g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
-            g2d.dispose();
+            // 校验压缩结果:必须小于原图且不为空才视为有效压缩
+            if (compressedBytes != null && compressedBytes.length < originalSize) {
+                log.debug("图片压缩成功,原图: {} bytes, 压缩后: {} bytes", originalSize, compressedBytes.length);
+                return compressedBytes;
+            }
 
-            // 对缩放后的图片进行质量压缩
-            return compressWithQuality(resizedImage, file.getOriginalFilename());
+            // 压缩无效,记录警告日志并返回null(调用方应使用原文件)
+            log.warn("图片压缩无效,压缩后大小({} bytes) >= 原图大小({} bytes)",
+                    compressedBytes != null ? compressedBytes.length : 0, originalSize);
+            return null;
         } catch (IOException e) {
-            log.error("图片压缩失败", e);
+            log.error("图片压缩失败,读取或处理图片时发生IO异常", e);
             return null;
         }
     }
 
     /**
-     * 图片质量压缩
-     * 使用 ImageWriter 进行高质量压缩,保持原格式
-     *
-     * @param image    待压缩的图片对象
-     * @param filename 原文件名,用于确定图片格式
-     * @return 压缩后的字节数组
+     * 迭代质量压缩
+     * 
+     * 核心算法:从 INITIAL_QUALITY(0.7) 开始逐步降低质量,每次降低 QUALITY_STEP(0.1),
+     * 直到压缩后文件小于原图或达到 MIN_QUALITY(0.1)。
+     * 
+     * 特殊处理:
+     * - 如果某质量档位压缩失败(返回null),继续尝试下一档位,不直接退出
+     * - 如果PNG格式所有质量档位均无效,自动尝试转为JPEG格式进行有损压缩
+     * 
+     * @param image        待压缩的图片对象(已完成尺寸缩放)
+     * @param format       图片格式(如 "jpg", "png")
+     * @param originalSize 原图大小(字节),用于判断压缩是否有效
+     * @return 压缩后的字节数组,若所有档位均无效则返回 null
+     */
+    private static byte[] compressWithIterativeQuality(BufferedImage image, String format, long originalSize) {
+        log.info("开始迭代压缩,格式: {}, 原图大小: {} bytes", format, originalSize);
+        
+        // 从初始质量开始迭代
+        float quality = INITIAL_QUALITY;
+
+        // 迭代循环:从高到低尝试不同质量档位
+        while (quality >= MIN_QUALITY) {
+            // 使用当前质量档位进行压缩
+            byte[] compressedBytes = compressWithQuality(image, format, quality);
+
+            // 压缩失败(返回null),继续尝试下一质量档位
+            if (compressedBytes == null) {
+                log.warn("compressWithQuality返回null, 格式: {}, 质量: {}", format, quality);
+                quality -= QUALITY_STEP;
+                continue;
+            }
+
+            // 记录当前压缩尝试结果
+            log.info("压缩尝试 - 格式: {}, 质量: {}, 压缩后大小: {} bytes", format, quality, compressedBytes.length);
+
+            // 判断压缩是否有效:压缩后大小 > 0 且 < 原图大小
+            if (compressedBytes.length > 0 && compressedBytes.length < originalSize) {
+                log.info("压缩成功,使用质量 {},压缩后大小 {} bytes", quality, compressedBytes.length);
+                return compressedBytes;
+            }
+
+            // 压缩无效,降低质量继续尝试
+            quality -= QUALITY_STEP;
+        }
+
+        // 所有质量档位均无效
+        log.warn("所有质量档位均无效,格式: {}", format);
+
+        // PNG格式特殊处理:尝试转为JPEG格式进行有损压缩
+        // JPEG的有损压缩通常能获得更好的压缩率,适合对透明度要求不高的场景
+        if ("png".equalsIgnoreCase(format)) {
+            log.info("尝试将PNG转为JPEG格式压缩");
+            byte[] jpegBytes = compressWithIterativeQuality(image, "jpg", originalSize);
+            if (jpegBytes != null && jpegBytes.length > 0 && jpegBytes.length < originalSize) {
+                log.info("PNG转JPEG压缩成功,原图: {} bytes, 压缩后: {} bytes", originalSize, jpegBytes.length);
+                return jpegBytes;
+            }
+            log.warn("PNG转JPEG压缩也失败");
+        }
+
+        // 所有压缩尝试均无效,返回null
+        return null;
+    }
+
+    /**
+     * 图片质量压缩(指定质量参数)
+     * 
+     * 使用 ImageWriter 进行高质量压缩,支持以下特性:
+     * 1. 透明通道处理:当输出格式为JPEG且原图有透明通道时,先转为白色背景的不透明图片
+     * 2. 压缩参数配置:通过 ImageWriteParam 设置显式压缩质量
+     * 3. 降级处理:如果 ImageWriter 不可用,使用 ImageIO.write 作为回退方案
+     * 
+     * @param image   待压缩的图片对象
+     * @param format  图片格式(如 "jpg", "png")
+     * @param quality 压缩质量(0.0-1.0),值越高质量越好但文件越大
+     * @return 压缩后的字节数组,所有方案均失败则返回 null
      */
-    private static byte[] compressWithQuality(BufferedImage image, String filename) {
+    private static byte[] compressWithQuality(BufferedImage image, String format, float quality) {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         try {
-            String format = getImageFormat(filename);
+            // 默认使用原图作为输出
+            BufferedImage outputImage = image;
+
+            // 特殊处理:JPEG格式不支持透明通道,需要先转为不透明图片
+            // 将透明区域填充为白色背景,避免透明部分显示为黑色
+            if ("jpg".equalsIgnoreCase(format) && image.getTransparency() != Transparency.OPAQUE) {
+                outputImage = convertToOpaque(image);
+            }
+
+            // 获取对应格式的 ImageWriter
             Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(format);
 
-            // 如果找不到对应格式的 ImageWriter,使用默认方式写入
+            // 如果找不到 ImageWriter,使用 ImageIO.write 作为回退方案
             if (!writers.hasNext()) {
-                ImageIO.write(image, format, baos);
+                ImageIO.write(outputImage, format, baos);
                 return baos.toByteArray();
             }
 
@@ -229,29 +340,69 @@ public class ImageUtil {
             ImageWriter writer = writers.next();
             ImageWriteParam writeParam = writer.getDefaultWriteParam();
 
+            // 设置显式压缩模式和质量参数
             if (writeParam.canWriteCompressed()) {
                 writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
-                writeParam.setCompressionQuality(QUALITY);
+                writeParam.setCompressionQuality(quality);
             }
 
+            // 写入压缩后的图片数据
             writer.setOutput(ImageIO.createImageOutputStream(baos));
-            writer.write(null, new javax.imageio.IIOImage(image, null, null), writeParam);
+            writer.write(null, new javax.imageio.IIOImage(outputImage, null, null), writeParam);
             writer.dispose();
 
-            return baos.toByteArray();
+            // 获取压缩结果并记录日志
+            byte[] result = baos.toByteArray();
+            log.debug("compressWithQuality完成 - 格式: {}, 质量: {}, 结果大小: {} bytes", format, quality, result.length);
+            return result;
         } catch (IOException e) {
-            log.error("图片质量压缩失败", e);
+            // 压缩失败,记录错误日志并尝试回退方案
+            log.error("图片质量压缩失败,格式: {}, 质量: {}", format, quality, e);
             try {
-                // 压缩失败时回退到普通写入方式
-                ImageIO.write(image, getImageFormat(filename), baos);
+                // 回退:使用 ImageIO.write 直接写入(不设置压缩参数)
+                ImageIO.write(image, format, baos);
                 return baos.toByteArray();
             } catch (IOException ex) {
+                // 回退也失败,返回null
                 log.error("图片回退保存失败", ex);
                 return null;
             }
         }
     }
 
+    /**
+     * 将带透明通道的图片转换为不透明图片
+     * 
+     * JPEG格式不支持透明通道,当需要将PNG等带透明通道的图片转为JPEG时,
+     * 需要先将透明区域填充为白色背景,避免透明部分显示为黑色。
+     * 
+     * @param image 带透明通道的图片
+     * @return 不透明图片(TYPE_INT_RGB),透明区域已填充为白色
+     */
+    private static BufferedImage convertToOpaque(BufferedImage image) {
+        // 创建不透明的RGB图片(TYPE_INT_RGB不支持透明通道)
+        BufferedImage opaqueImage = new BufferedImage(
+                image.getWidth(),
+                image.getHeight(),
+                BufferedImage.TYPE_INT_RGB
+        );
+        
+        // 创建 Graphics2D 对象用于绘制
+        Graphics2D g2d = opaqueImage.createGraphics();
+        
+        // 步骤1:填充白色背景,覆盖透明区域
+        g2d.setColor(Color.WHITE);
+        g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
+        
+        // 步骤2:绘制原图,此时透明区域已被白色背景覆盖
+        g2d.drawImage(image, 0, 0, null);
+        
+        // 释放资源
+        g2d.dispose();
+        
+        return opaqueImage;
+    }
+
     /**
      * 根据文件名获取图片格式
      *

+ 69 - 17
platform/src/main/java/com/jzg/controller/FileUploadController.java

@@ -12,6 +12,7 @@ import com.jzg.commons.util.idgen.IdGenerate;
 import com.jzg.mapper.SysUploadFilesMapper;
 import io.minio.*;
 import io.swagger.v3.oas.annotations.Operation;
+import lombok.extern.slf4j.Slf4j;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -31,6 +32,7 @@ import java.time.LocalDateTime;
 
 @RestController
 @RequestMapping("/file")
+@Slf4j
 public class FileUploadController extends BaseController {
 
 
@@ -57,35 +59,72 @@ public class FileUploadController extends BaseController {
     public HttpResult uploadFile(@RequestParam("file") MultipartFile file) {
         System.out.printf("调用文件上传接口");
         try {
-            // 获取文件原始名称和扩展名
+            // 获取文件原始名称,用于后续生成对象名
             String originalFileName = file.getOriginalFilename();
+            // 获取文件扩展名(含点号,如 ".jpg"),用于判断文件类型和生成对象名
             String extension = getFileExtension(originalFileName);
+            // 获取文件原始大小,用于压缩效果对比
+            long originalSize = file.getSize();
+            // 获取文件的 Content-Type,用于判断是否为图片类型
+            String contentType = file.getContentType();
 
-            // 判断是否为图片文件,若是则进行压缩
-            // 双重校验:通过 Content-Type 和文件扩展名判断
+            // 压缩后的字节数组,初始为null表示未压缩或压缩失败
             byte[] compressedBytes = null;
-            if (ImageUtil.isImage(file.getContentType()) || ImageUtil.isImageExtension(extension)) {
+            // 压缩后的文件扩展名,默认使用原扩展名,PNG转JPEG时会更新
+            String compressedExtension = extension;
+            // 压缩后的 Content-Type,默认使用原类型,PNG转JPEG时会更新
+            String compressedContentType = contentType;
+
+            // 判断是否为图片文件(双重校验:Content-Type 和扩展名)
+            // 图片文件需要进行压缩处理,非图片文件直接上传
+            if (ImageUtil.isImage(contentType) || ImageUtil.isImageExtension(extension)) {
                 compressedBytes = ImageUtil.compressImage(file);
             }
 
-            // 根据是否压缩成功,计算文件的 MD5 和 ETag
-            // 注意:MD5/ETag 必须基于最终上传的文件内容计算,确保与 MinIO 存储一致
-            String fileMd5;
-            String fileEtag;
-            String objectName;
+            // 检查压缩结果:压缩成功且压缩后文件不为空
+            if (compressedBytes != null && compressedBytes.length > 0) {
+                // 打印压缩前后大小对比,便于调试和监控压缩效果
+                log.info("图片压缩前大小: {} bytes, 压缩后大小: {} bytes", originalSize, compressedBytes.length);
+
+                // 判断压缩是否有效:如果压缩后文件比原图大或相等,则视为压缩无效
+                // 此时回退到使用原文件上传,避免上传更大的文件
+                if (compressedBytes.length >= originalSize) {
+                    log.info("警告:压缩后图片({} bytes)比原图{} bytes)大,使用原图片上传", compressedBytes.length, originalSize);
+                    compressedBytes = null;
+                }
+            }
 
+            // 声明文件哈希值和对象名变量
+            String fileMd5;      // 文件MD5值,用于唯一标识文件
+            String fileEtag;     // 文件ETag值,用于MinIO文件校验
+            String objectName;   // MinIO中的对象名称
+
+            // 根据是否压缩成功,选择不同的处理路径
             if (compressedBytes != null && compressedBytes.length > 0) {
-                // 使用压缩后的字节数组计算哈希值
+                // 压缩成功:基于压缩后的字节数组计算哈希值
                 fileMd5 = FileUtils.getFileMD5(compressedBytes);
                 fileEtag = FileUtils.getFileETag(compressedBytes);
-                objectName = generateObjectName(originalFileName, fileMd5, extension);
+
+                // 特殊处理:当原文件是PNG格式,但压缩后实际是JPEG格式时
+                // 需要更新扩展名和Content-Type,确保文件类型与内容一致
+                // 这是因为ImageUtil在PNG压缩无效时会尝试转为JPEG格式
+                if (".png".equalsIgnoreCase(extension) && isJpegContent(compressedBytes)) {
+                    compressedExtension = ".jpg";
+                    compressedContentType = "image/jpeg";
+                    log.info("PNG图片转换为JPEG格式,扩展名从 {} 变为 {}", extension, compressedExtension);
+                }
+
+                // 使用压缩后的扩展名生成MinIO对象名
+                objectName = generateObjectName(originalFileName, fileMd5, compressedExtension);
             } else {
-                // 使用原始文件计算哈希值(非图片或压缩失败时回退)
+                // 压缩失败或非图片文件:基于原文件计算哈希值
                 fileMd5 = FileUtils.getFileMD5(file);
                 fileEtag = FileUtils.getFileETag(file);
+                // 使用原扩展名生成MinIO对象名
                 objectName = generateObjectName(originalFileName, fileMd5, extension);
             }
 
+//            // 检查文件是否已存在于MinIO(功能已注释,如需启用请取消注释)
 //            if (isFileExistsInMinIo(objectName, fileEtag)) {
 //                return HttpResult.ok(getFileFromDatabase(fileEtag,objectName, extension));
 //            }
@@ -93,8 +132,8 @@ public class FileUploadController extends BaseController {
             // 根据是否压缩成功,选择对应的上传方式
             SysUploadFiles sysUploadFiles;
             if (compressedBytes != null && compressedBytes.length > 0) {
-                // 上传压缩后的图片字节数组
-                sysUploadFiles = uploadFileToMinIo(compressedBytes, objectName, fileEtag, extension, file.getContentType());
+                // 上传压缩后的图片字节数组(使用更新后的扩展名和Content-Type)
+                sysUploadFiles = uploadFileToMinIo(compressedBytes, objectName, fileEtag, compressedExtension, compressedContentType);
             } else {
                 // 上传原始文件(非图片或压缩失败时回退)
                 sysUploadFiles = uploadFileToMinIo(file, objectName, fileEtag, extension);
@@ -106,6 +145,22 @@ public class FileUploadController extends BaseController {
         }
     }
 
+    /**
+     * 判断字节数组是否为JPEG格式的图片内容
+     * 通过检测文件魔数(Magic Number)来判断:JPEG的魔数是 FF D8 FF
+     *
+     * @param data 待检测的字节数组
+     * @return 是否为JPEG格式内容
+     */
+    private boolean isJpegContent(byte[] data) {
+        // 参数校验:数据为空或长度不足3字节,无法包含完整的JPEG魔数
+        if (data == null || data.length < 3) {
+            return false;
+        }
+        // JPEG格式魔数检测:FF D8 FF
+        return data[0] == (byte) 0xFF && data[1] == (byte) 0xD8 && data[2] == (byte) 0xFF;
+    }
+
     @PostMapping("/uploadUrl")
     @Operation(summary = "网络文件上传接口", description = "文件上传接口")
     public HttpResult<SysUploadFiles> uploadUrlFile(String urlString, String fileName) {
@@ -232,7 +287,6 @@ public class FileUploadController extends BaseController {
                 .stream(fileInputStream, file.getSize(), -1)
                 .contentType(file.getContentType())
                 .build());
-
         return insertSysUploadFiles(objectName, extension, objectWriteResponse);
     }
 
@@ -244,7 +298,6 @@ public class FileUploadController extends BaseController {
                 .stream(fileInputStream, fileBytes.length, -1)
                 .contentType(contentType)
                 .build());
-
         return insertSysUploadFiles(objectName, extension, objectWriteResponse);
     }
 
@@ -257,7 +310,6 @@ public class FileUploadController extends BaseController {
                 .stream(fileInputStream, contentLength, -1)
                 .contentType(contentType)
                 .build());
-
         return insertSysUploadFiles(objectName, extension, objectWriteResponse);
     }
 

+ 6 - 6
tenant/insurance/quotation-huatai/src/main/java/com/jzg/quotation/huatai/crawler/entity/Request/HtCrawlerSubmitRequest.java

@@ -29,7 +29,7 @@ public class HtCrawlerSubmitRequest implements Serializable {
         List<InsOrdersCarUserInfo> insOrdersCarUserInfoList = underwritingVo.getInsOrdersCarUserInfoList();
 
         InsOrdersCarUserInfo ownerInfo = null;
-        InsOrdersCarUserInfo applicantInfo = null;
+        InsOrdersCarUserInfo policyInfo = null;
         InsOrdersCarUserInfo insuredInfo = null;
 
         for (InsOrdersCarUserInfo userInfo : insOrdersCarUserInfoList) {
@@ -38,7 +38,7 @@ public class HtCrawlerSubmitRequest implements Serializable {
                     ownerInfo = userInfo;
                     break;
                 case 2:
-                    applicantInfo = userInfo;
+                    policyInfo = userInfo;
                     break;
                 case 3:
                     insuredInfo = userInfo;
@@ -130,10 +130,10 @@ public class HtCrawlerSubmitRequest implements Serializable {
             clauseDTOS.add(clauseDTO);
         }
 
-        boolean isOwnerSameAsInsured = (ownerInfo != null && insuredInfo != null &&
-                ownerInfo.getName().equals(insuredInfo.getName()));
+        boolean isOwnerSameAsPolicy = (ownerInfo != null && policyInfo != null &&
+                ownerInfo.getName().equals(policyInfo.getName()));
 
-        if (isOwnerSameAsInsured) {
+        if (isOwnerSameAsPolicy) {
             this.iisCarOwner = "Y";
             this.beibaorenId = "Y";
         }
@@ -147,7 +147,7 @@ public class HtCrawlerSubmitRequest implements Serializable {
         jxClause.setReadonly("1");
         clauseDTOS.add(jxClause);
 
-        if (!isOwnerSameAsInsured && ownerInfo != null) {
+        if (!isOwnerSameAsPolicy && ownerInfo != null) {
             ClauseDTO ownerClause = new ClauseDTO();
             ownerClause.setChosedE("T0028");
             ownerClause.setClausename("车主约定");

+ 23 - 0
tenant/insurance/quotation-huatai/src/main/java/com/jzg/quotation/huatai/crawler/entity/po/HtCrawlerOrder.java

@@ -33,4 +33,27 @@ public class HtCrawlerOrder {
      * 核保额外需要的参数  商业评分
      */
     private String biCategory;
+    /**
+     * 交强险保单号
+     */
+    private String jqPolicyno;
+
+    /**
+     * 商业保单号
+     */
+    private String syPolicyno;
+
+    /**
+     * 驾意保单号
+     */
+    private String jyPolicyno;
+    /**
+     * 支付链接
+     */
+    private String paymentLink;
+    /**
+     * 投保人姓名
+     */
+    private String policyName;
+
 }

+ 158 - 13
tenant/insurance/quotation-huatai/src/main/java/com/jzg/quotation/huatai/crawler/service/Impl/HuaTaiCrawlerRequestImpl.java

@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import com.jzg.commons.constants.ProductsType;
 import com.jzg.commons.constants.dict.InsOrderStatusEnum;
+import com.jzg.commons.entity.orders.po.InsOrdersCarUserInfo;
 import com.jzg.commons.entity.po.jyxSetting;
 import com.jzg.commons.entity.quote.vo.AccidentalDrivingVo;
 import com.jzg.commons.entity.quote.vo.AttributionInformationVo;
@@ -48,8 +49,10 @@ import com.jzg.quotation.huatai.crawler.util.CryptoUtil;
 import com.jzg.quotation.huatai.crawler.util.UploadUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
@@ -865,15 +868,24 @@ public class HuaTaiCrawlerRequestImpl implements HuaTaiCrawlerRequest {
 
     private UnderwritingResultsVo handlePassedUnderwriting(HtCrawlerOrder order, HtCrawlerConfigureParameters params, UnderwritingVo vo, Map<String, Object> map) {
         HtCrawlerPaymentLinkResponse pl = queryPaymentLink(order, params, new PaymentLinkVo());
-        log.info("paymentLinkResponse: {}", JSONObject.toJSONString(pl));
-        vo.getInsOrders().setPayLink(pl.getImg());
-        vo.getInsOrders().setAuditTime(LocalDateTime.now());
-        vo.getInsOrders().setCompanyOrderNo(order.getQuotationRelaNo());
+        log.info("handlePassedUnderwriting_paymentLinkResponse: {}", JSONObject.toJSONString(pl));
         UnderwritingResultsVo r = new UnderwritingResultsVo();
         r.setPaymentLink(pl.getImg());
         r.setJqPolicyno(map.get("jqProposalno") == null ? "" : map.get("jqProposalno").toString());
         r.setSyPolicyno(map.get("syProposalno") == null ? "" : map.get("syProposalno").toString());
         r.setUnderwritingStatus(InsOrderStatusEnum.PAY_PENDING);
+
+        order.setJqPolicyno(r.getJqPolicyno());
+        order.setSyPolicyno(r.getSyPolicyno());
+        InsOrdersCarUserInfo insOrdersCarUserInfo = null;
+        for (InsOrdersCarUserInfo userInfo : vo.getInsOrdersCarUserInfoList()) {
+            if(userInfo.getPolicyPersonType() == 2) {
+                insOrdersCarUserInfo = userInfo;
+                order.setPolicyName(insOrdersCarUserInfo.getName());
+                break;
+            }
+        }
+        orderService.update(order);
         return r;
     }
 
@@ -887,6 +899,8 @@ public class HuaTaiCrawlerRequestImpl implements HuaTaiCrawlerRequest {
                 HtCrawlerConfigureParameters.class, attr.getConfigInfo());
         HtCrawlerPaymentLinkResponse pl = queryPaymentLink(order, params, vo);
         log.info("paymentLinkResponse: {}", JSONObject.toJSONString(pl));
+        order.setPaymentLink(pl.getImg());
+        orderService.update(order);
         return new PaymentLinkResultsVo(true, pl.getImg(), pl.getErrorInfo());
     }
 
@@ -975,12 +989,12 @@ public class HuaTaiCrawlerRequestImpl implements HuaTaiCrawlerRequest {
         AttributionInformationVo attr = vo.getOrder().getAttributionInformationVo();
         doLoginCookie(attr);
         String productId = vo.getOrder().getProductId();
-        String companyOrderNo = vo.getOrder().getCompanyOrderNo();
+        HtCrawlerOrder order = orderService.findByOrdersNo(vo.getOrder().getOrdersNo());
         return ProductsType.PT_0330.getCode().equals(productId)
-                ? downloadPolicy(companyOrderNo, true) : downloadPolicy(companyOrderNo, false);
+                ? downloadPolicy(vo, order, order.getJqPolicyno(), true) : downloadPolicy(vo, order, order.getSyPolicyno(), false);
     }
 
-    private DownloadPolicyResultVo downloadPolicy(String no, boolean jq) {
+    private DownloadPolicyResultVo downloadPolicy(DownloadPolicyVo vo,HtCrawlerOrder order, String no, boolean jq) {
         MultiValueMap<String, String> fd = new LinkedMultiValueMap<>();
         fd.add("businessno", no);
         fd.add("riskcode", jq ? "1002" : "1251");
@@ -996,38 +1010,169 @@ public class HuaTaiCrawlerRequestImpl implements HuaTaiCrawlerRequest {
         HtCrawlerDownloadPolicyResponse res = JSONObject.parseObject(body, HtCrawlerDownloadPolicyResponse.class);
         log.info("下载保单响应结果,{}", JSONObject.toJSONString(res));
         DownloadPolicyResultVo r = new DownloadPolicyResultVo();
-        for (var ftp : res.getFtpdownloadJson()) {
-            if (jq) { r.setJqUrl(ftp.getDownurl()); r.setJqFlagUrl(ftp.getBzdownurl()); }
-            else { r.setSyUrl(ftp.getDownurl()); }
-            log.info("下载{}保单地址:{}", jq ? "交强" : "商业", ftp.getDownurl());
+        List<HtCrawlerDownloadPolicyResponse.FtpdownloadJson> ftpdownloadJson = res.getFtpdownloadJson();
+        if(!ftpdownloadJson.isEmpty()) {
+            if (jq) {
+                r.setJqUrl(ftpdownloadJson.get(0).getDownurl());
+                r.setJqFlagUrl(ftpdownloadJson.get(1).getBzdownurl());
+                log.info("下载交强保单地址:{}", r.getJqUrl());
+            } else {
+                r.setSyUrl(ftpdownloadJson.get(0).getDownurl());
+                log.info("下载商业保单地址:{}", r.getSyUrl());
+            }
+        }
+        if (StringUtils.isNotBlank(order.getJyPolicyno())) {
+            // 1. 构造请求头
+            HttpHeaders headers = getHeaders();
+
+            // 2. url + query params(cmd=perDocumentPrint)
+            String baseUrl = "https://cspxp.pc.ehuatai.com";
+            String path = "/htcsp/DocumentPrintAction.do";
+            UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(baseUrl + path);
+            uriBuilder.queryParam("cmd", "perDocumentPrint");
+            String fullUrl = uriBuilder.toUriString();
+
+            // 3. 构造POST表单data(MultiValueMap 对应 form-data)
+            MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+            formData.add("businessno", order.getJyPolicyno() + ",");
+            formData.add("registeredxml", "");
+            formData.add("printSysflag", "7,");
+            AttributionInformationVo attr = vo.getOrder().getAttributionInformationVo();
+            HtCrawlerConfigureParameters params = ConfigureParametersConversion.conversionEntity(
+                    HtCrawlerConfigureParameters.class, attr.getConfigInfo());
+            // Businesscompanynodecode 传两个相同comCode,表单同一个key传多次
+            formData.add("Businesscompanynodecode", params.getComCode());
+            formData.add("Businesscompanynodecode", params.getComCode());
+            formData.add("productcode", "E007");
+            formData.add("noncarPrintflag", "10");
+            formData.add("documenttype", "3");
+            formData.add("documenttypecode", "");
+            formData.add("documentstartcode", "");
+            // peopleinfo 取 tb_uname
+            String tbUname = order.getPolicyName();
+            formData.add("payrefname", tbUname);
+
+            // 4. 发起POST 请求(复用类内已有的post工具方法)
+            String responseText = post(fullUrl, headers, formData);
+            log.info("feiche response:{}", responseText);
+
+            // 5. 解析返回JSON
+            Map<String, Object> respMap = null;
+            try {
+                respMap = MAPPER.readValue(responseText, new TypeReference<Map<String, Object>>() {});
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e);
+            }
+            String printUrl = String.valueOf(respMap.get("printurl"));
+            // 替换 & 为 @
+            printUrl = printUrl.replace("&", "@");
+            // 拼接固定前缀赋值成员变量
+            String jyxPolicyUrl = "http://39.98.41.104:8088/ht/ht_bddownload?values=" + printUrl;
+            r.setJyUrl(jyxPolicyUrl);
         }
         return r;
     }
 
+    @NotNull
+    private HttpHeaders getHeaders() {
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Accept", "application/json, text/javascript, */*; q=0.01");
+        headers.set("Accept-Language", "zh-CN,zh;q=0.9");
+        headers.set("Connection", "keep-alive");
+        headers.set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+        headers.set("Origin", "https://cspxp.pc.ehuatai.com");
+        headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
+        headers.set("X-Requested-With", "XMLHttpRequest");
+        headers.set("Cookie", req.getCookie());
+        return headers;
+    }
+
     // ====================== 查询订单状态 ======================
     @Override
     public OrderStatusResultVo queryOrderState(OrderStatusVo vo) {
         AttributionInformationVo attr = vo.getOrder().getAttributionInformationVo();
+        doLoginCookie(attr);
         HtCrawlerConfigureParameters params = ConfigureParametersConversion.conversionEntity(
                 HtCrawlerConfigureParameters.class, attr.getConfigInfo());
         HtCrawlerOrder order = orderService.findByOrdersNo(vo.getOrder().getOrdersNo());
-        String orderStatus = req.queryOrderStatus(new HtCrawlerOrderStateRequest(params, order.getQuotationRelaNo()), req.getHeaders());
+        String html = req.queryOrderStatus(new HtCrawlerOrderStateRequest(params, order.getQuotationRelaNo()), req.getHeaders());
         String regex = "<div style=\"display: inline-block;width:50px;text-align: center; \">(.*?)</div>";
         Pattern pattern = Pattern.compile(regex);
-        Matcher matcher = pattern.matcher(orderStatus);
+        Matcher matcher = pattern.matcher(html);
+        String orderStatus = null;
         if (matcher.find()) orderStatus = matcher.group(1);
         InsOrderStatusEnum orderStatusEnum = null;
+        String syApplication = "";
+        String jqApplication = "";
+        String fcxApplication = "";
         if ("已通过核保已实收".equals(orderStatus)) {
             orderStatusEnum = InsOrderStatusEnum.valueOf("QUOTE_ING");
             orderStatus = orderStatusEnum.getCode();
+            List<Map<String, String>> fcxList = new ArrayList<>();
+            // 1. 构建请求头 headers
+            HttpHeaders headers = getHttpHeaders();
+
+            // 2. 拼接URL + GET参数 params
+            String baseUrl = "https://cspxp.pc.ehuatai.com/htcsp/DocumentPrintAction.do";
+            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
+            map.add("cmd", "queryDocumentPrintList");
+            map.add("printType", "1");
+            map.add("quotationno", order.getQuotationRelaNo());
+            map.add("licenseno", vo.getCarInfo().getLicenseNo());
+
+            // 3. 发起GET请求(复用项目已有cookieUtils工具,跳过证书校验同verify=False)
+            String htmlResp = get(baseUrl, headers, map);
+
+            // 4. Jsoup解析HTML,查找所有type=checkbox的input
+            Document soup = Jsoup.parse(htmlResp);
+            Elements inputElements = soup.select("input[type=checkbox]");
+
+            // 循环遍历input标签
+            for (Element item : inputElements) {
+                String businessno = item.attr("businessno");
+                String riskcode = item.attr("riskcode");
+                log.info("riskcode: {}, businessno: {}", riskcode, businessno);
+
+                if ("1251".equals(riskcode)) {
+                    syApplication = businessno;
+                } else if ("1002".equals(riskcode)) {
+                    jqApplication = businessno;
+                } else {
+                    fcxApplication = businessno;
+                    Map<String, String> fcxItem = new HashMap<>();
+                    fcxItem.put("policy", "");
+                    fcxItem.put("application", businessno);
+                    fcxList.add(fcxItem);
+                }
+            }
         }
         OrderStatusResultVo orderStatusResultVo = new OrderStatusResultVo();
         //将订单状态转换为枚举类型
         orderStatusResultVo.setOrderStatus(orderStatusEnum);
         orderStatusResultVo.setOrderStatusStr(orderStatus);
+
+        orderStatusResultVo.setJqPolicyno( StringUtils.isBlank(jqApplication) ? order.getJqPolicyno() : jqApplication);
+        orderStatusResultVo.setSyPolicyno( StringUtils.isBlank(syApplication) ? order.getSyPolicyno() : syApplication);
+        orderStatusResultVo.setJyPolicyno( StringUtils.isBlank(fcxApplication) ? order.getJyPolicyno() : fcxApplication);
+        orderStatusResultVo.setPaymentLink(order.getPaymentLink());
         return orderStatusResultVo;
     }
 
+    @NotNull
+    private HttpHeaders getHttpHeaders() {
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
+        headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
+        headers.set("Cache-Control", "no-cache");
+        headers.set("Connection", "keep-alive");
+        headers.set("Pragma", "no-cache");
+        headers.set("Referer", "https://cspxp.pc.ehuatai.com/htcsp/QuotationQueryAction.do?cmd=quotationListAll");
+        headers.set("Upgrade-Insecure-Requests", "1");
+        headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36");
+        headers.set("Cookie", req.getCookie()); // 塞入cookie
+        return headers;
+    }
+
     // ====================== Headers 统一构建 ======================
     private HttpHeaders header(Consumer<HttpHeaders> customizer) {
         HttpHeaders h = new HttpHeaders();