Преглед на файлове

Merge branch 'release_lutong_bug' into sit

lutong преди 2 месеца
родител
ревизия
0fd957a823

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOperationalStatisticsHistory.java

@@ -48,6 +48,10 @@ public class StoreOperationalStatisticsHistory {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date queryTime;
 
+    @ApiModelProperty(value = "PDF文件URL(前端生成的PDF文件地址)")
+    @TableField("pdf_url")
+    private String pdfUrl;
+
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreOperationalStatisticsComparisonVo.java

@@ -32,6 +32,9 @@ public class StoreOperationalStatisticsComparisonVo implements Serializable {
     @ApiModelProperty("上期结束时间")
     private String previousEndTime;
 
+    @ApiModelProperty("历史记录ID(用于后续更新PDF URL)")
+    private Integer historyId;
+
     /**
      * 流量数据对比
      */

+ 176 - 2
alien-store/src/main/java/shop/alien/store/controller/StoreFileUploadController.java

@@ -1,17 +1,22 @@
 package shop.alien.store.controller;
 
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiOperationSupport;
 import io.swagger.annotations.ApiSort;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.result.R;
 import shop.alien.store.util.FileUploadUtil;
+import shop.alien.store.util.ImageToPdfUploadUtil;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -31,8 +36,10 @@ import java.util.List;
 public class StoreFileUploadController {
 
     private final FileUploadUtil fileUpload;
+    
+    private final ImageToPdfUploadUtil imageToPdfUploadUtil;
 
-    @ApiOperation("单个文件上传(图片或视频,返回路径)")
+    @ApiOperation("单个文件上传(图片、视频或PDF,返回路径)")
     @ApiOperationSupport(order = 1)
     @PostMapping("/upload")
     public R<String> upload(@RequestParam("file") MultipartFile file) {
@@ -40,7 +47,7 @@ public class StoreFileUploadController {
         return R.data(fileUpload.uploadOneFile(file));
     }
 
-    @ApiOperation("多个文件上传(图片或视频,视频会截取第一秒图片,返回路径)")
+    @ApiOperation("多个文件上传(图片、视频或PDF,视频会截取第一秒图片,返回路径)")
     @ApiOperationSupport(order = 2)
     @PostMapping("/uploadMore")
     public R<List<String>> uploadMore(MultipartRequest multipartRequest) {
@@ -63,4 +70,171 @@ public class StoreFileUploadController {
         return R.data(fileUpload.uploadApp(file));
     }
 
+    @ApiOperation("上传PDF文件(返回路径)")
+    @ApiOperationSupport(order = 5)
+    @PostMapping("/uploadPdf")
+    public R<String> uploadPdf(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.uploadPdf fileName:{}", file.getOriginalFilename());
+        try {
+            String filePath = fileUpload.uploadPdf(file);
+            if (filePath == null) {
+                return R.fail("上传失败,文件不是PDF格式");
+            }
+            return R.data(filePath);
+        } catch (Exception e) {
+            log.error("上传PDF文件失败: {}", e.getMessage(), e);
+            return R.fail("上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(单张图片,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "file",
+                    value = "图片文件(支持jpg、jpeg、png、bmp、webp、gif格式)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/imageToPdf")
+    public R<String> imageToPdf(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.imageToPdf fileName:{}", file.getOriginalFilename());
+        try {
+            // 参数验证
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要转换的图片文件");
+            }
+            
+            String pdfUrl = imageToPdfUploadUtil.imageToPdfAndUpload(file);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(多张图片合并为一个PDF,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "files",
+                    value = "图片文件列表(支持jpg、jpeg、png、bmp、webp、gif格式,可上传多张)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true,
+                    allowMultiple = true
+            )
+    })
+    @PostMapping("/imagesToPdf")
+    public R<String> imagesToPdf(MultipartRequest multipartRequest) {
+        log.info("StoreFileUploadController.imagesToPdf");
+        try {
+            if (multipartRequest == null) {
+                return R.fail("请求参数不能为空");
+            }
+            
+            List<MultipartFile> imageFiles = new ArrayList<>();
+            for (String key : multipartRequest.getFileMap().keySet()) {
+                MultipartFile file = multipartRequest.getFile(key);
+                if (file != null && !file.isEmpty()) {
+                    imageFiles.add(file);
+                }
+            }
+            
+            if (imageFiles.isEmpty()) {
+                return R.fail("请至少上传一张图片");
+            }
+            
+            log.info("StoreFileUploadController.imagesToPdf 收到{}张图片", imageFiles.size());
+            
+            String pdfUrl = imageToPdfUploadUtil.imagesToPdfAndUpload(imageFiles);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(A4页面大小,单张图片,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 8)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "file",
+                    value = "图片文件(支持jpg、jpeg、png、bmp、webp、gif格式,图片会自动缩放适应A4页面)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/imageToPdfA4")
+    public R<String> imageToPdfA4(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.imageToPdfA4 fileName:{}", file.getOriginalFilename());
+        try {
+            // 参数验证
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要转换的图片文件");
+            }
+            
+            String pdfUrl = imageToPdfUploadUtil.imageToPdfA4AndUpload(file);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("Base64图片转PDF并上传(返回PDF的线上URL)")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "base64Image",
+                    value = "Base64编码的图片字符串(支持带或不带data:image前缀,如:data:image/png;base64,iVBORw0KGgo...)",
+                    dataType = "String",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/base64ToPdf")
+    public R<String> base64ToPdf(@RequestParam("base64Image") String base64Image) {
+        log.info("StoreFileUploadController.base64ToPdf 收到Base64图片");
+        try {
+            // 参数验证
+            if (!StringUtils.hasText(base64Image)) {
+                return R.fail("Base64图片字符串不能为空");
+            }
+
+            String pdfUrl = imageToPdfUploadUtil.base64ToPdfAndUpload(base64Image);
+            if (pdfUrl == null) {
+                return R.fail("Base64图片转PDF并上传失败,请检查Base64格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("Base64图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("Base64图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("Base64图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
 }

+ 44 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreOperationalStatisticsController.java

@@ -228,4 +228,48 @@ public class StoreOperationalStatisticsController {
             return R.fail("批量删除失败: " + e.getMessage());
         }
     }
+
+    @ApiOperation("更新历史统计记录的PDF URL")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "historyId",
+                    value = "历史记录ID",
+                    dataType = "Integer",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "pdfUrl",
+                    value = "PDF文件URL(前端生成的PDF文件地址)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            )
+    })
+    @PutMapping("/history/updatePdfUrl")
+    public R<String> updateHistoryPdfUrl(
+            @RequestParam("historyId") Integer historyId,
+            @RequestParam("pdfUrl") String pdfUrl) {
+        log.info("StoreOperationalStatisticsController.updateHistoryPdfUrl - historyId={}, pdfUrl={}", historyId, pdfUrl);
+        try {
+            if (historyId == null || historyId <= 0) {
+                return R.fail("历史记录ID不能为空且必须大于0");
+            }
+            
+            if (pdfUrl == null || pdfUrl.trim().isEmpty()) {
+                return R.fail("PDF URL不能为空");
+            }
+            
+            boolean result = storeOperationalStatisticsService.updateHistoryPdfUrl(historyId, pdfUrl);
+            if (result) {
+                return R.success("更新PDF URL成功");
+            } else {
+                return R.fail("更新PDF URL失败,历史记录不存在或已被删除");
+            }
+        } catch (Exception e) {
+            log.error("更新历史统计记录PDF URL失败 - historyId={}, pdfUrl={}, error={}", historyId, pdfUrl, e.getMessage(), e);
+            return R.fail("更新PDF URL失败: " + e.getMessage());
+        }
+    }
 }

+ 9 - 0
alien-store/src/main/java/shop/alien/store/service/StoreOperationalStatisticsService.java

@@ -71,4 +71,13 @@ public interface StoreOperationalStatisticsService {
      * @return 经营统计数据
      */
     StoreOperationalStatisticsVo getStatisticsInTrackFormat(Integer storeId, String startTime, String endTime);
+
+    /**
+     * 更新历史统计记录的PDF URL
+     *
+     * @param historyId 历史记录ID
+     * @param pdfUrl    PDF文件URL
+     * @return 是否成功
+     */
+    boolean updateHistoryPdfUrl(Integer historyId, String pdfUrl);
 }

+ 70 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java

@@ -172,7 +172,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setPreviousStartTime(previousStartTime);
         comparison.setPreviousEndTime(previousEndTime);
 
-        // 获取当期和上期的统计数据(不保存历史,对比接口不需要保存)
+        // 获取当期和上期的统计数据
         StoreOperationalStatisticsVo currentStatistics = calculateStatistics(storeId, currentStartTime, currentEndTime);
         StoreOperationalStatisticsVo previousStatistics = calculateStatistics(storeId, previousStartTime, previousEndTime);
 
@@ -183,6 +183,14 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setVoucherData(buildVoucherDataComparison(currentStatistics.getVoucherData(), previousStatistics.getVoucherData()));
         comparison.setServiceQualityData(buildServiceQualityDataComparison(currentStatistics.getServiceQualityData(), previousStatistics.getServiceQualityData()));
 
+        // 保存对比数据到历史表,获取历史记录ID
+        Integer historyId = saveStatisticsHistory(storeId, currentStartTime, currentEndTime, comparison);
+        
+        // 将历史记录ID设置到返回对象中,方便前端后续更新PDF URL
+        if (historyId != null) {
+            comparison.setHistoryId(historyId);
+        }
+
         return comparison;
     }
 
@@ -821,20 +829,21 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         // 计算统计数据
         StoreOperationalStatisticsVo statistics = calculateStatistics(storeId, startTime, endTime);
         
-        // 保存统计数据到历史表
-        saveStatisticsHistory(storeId, startTime, endTime, statistics);
+        // 不再保存统计数据到历史表,只有对比接口才会保存历史数据
         
         return statistics;
     }
     
     /**
-     * 保存统计数据到历史表
+     * 保存对比统计数据到历史表
+     * 仅在调用 getStatisticsComparison 接口时触发
+     * @return 历史记录ID,保存失败返回null
      */
-    private void saveStatisticsHistory(Integer storeId, String startTime, String endTime, StoreOperationalStatisticsVo statistics) {
+    private Integer saveStatisticsHistory(Integer storeId, String currentStartTime, String currentEndTime, StoreOperationalStatisticsComparisonVo comparison) {
         try {
             SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
-            Date startDate = sdf.parse(startTime);
-            Date endDate = sdf.parse(endTime);
+            Date startDate = sdf.parse(currentStartTime);
+            Date endDate = sdf.parse(currentEndTime);
 
             StoreOperationalStatisticsHistory history = new StoreOperationalStatisticsHistory();
             history.setStoreId(storeId);
@@ -842,16 +851,19 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             history.setEndTime(endDate);
             history.setQueryTime(new Date());
             
-            // 将统计数据序列化为JSON字符串
-            String statisticsJson = JSON.toJSONString(statistics);
+            // 将对比统计数据序列化为JSON字符串(包含当期、上期和变化率等完整对比数据)
+            String statisticsJson = JSON.toJSONString(comparison);
             history.setStatisticsData(statisticsJson);
 
             statisticsHistoryMapper.insert(history);
-            log.info("保存统计数据到历史表成功 - storeId={}, startTime={}, endTime={}", storeId, startTime, endTime);
+            log.info("保存对比统计数据到历史表成功 - storeId={}, currentStartTime={}, currentEndTime={}, historyId={}", 
+                    storeId, currentStartTime, currentEndTime, history.getId());
+            return history.getId();
         } catch (Exception e) {
-            log.error("保存统计数据到历史表失败 - storeId={}, startTime={}, endTime={}, error={}", 
-                    storeId, startTime, endTime, e.getMessage(), e);
+            log.error("保存对比统计数据到历史表失败 - storeId={}, currentStartTime={}, currentEndTime={}, error={}", 
+                    storeId, currentStartTime, currentEndTime, e.getMessage(), e);
             // 保存失败不影响主流程,只记录日志
+            return null;
         }
     }
 
@@ -909,6 +921,52 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             return false;
         }
     }
+
+    @Override
+    public boolean updateHistoryPdfUrl(Integer historyId, String pdfUrl) {
+        log.info("StoreOperationalStatisticsServiceImpl.updateHistoryPdfUrl - historyId={}, pdfUrl={}", historyId, pdfUrl);
+        
+        if (historyId == null || historyId <= 0) {
+            log.warn("更新历史统计记录PDF URL失败,历史记录ID无效 - historyId={}", historyId);
+            return false;
+        }
+        
+        if (pdfUrl == null || pdfUrl.trim().isEmpty()) {
+            log.warn("更新历史统计记录PDF URL失败,PDF URL为空 - historyId={}", historyId);
+            return false;
+        }
+        
+        try {
+            // 查询历史记录是否存在且未删除
+            StoreOperationalStatisticsHistory history = statisticsHistoryMapper.selectOne(
+                    new LambdaQueryWrapper<StoreOperationalStatisticsHistory>()
+                            .eq(StoreOperationalStatisticsHistory::getId, historyId)
+                            .eq(StoreOperationalStatisticsHistory::getDeleteFlag, 0));
+            
+            if (history == null) {
+                log.warn("更新历史统计记录PDF URL失败,历史记录不存在或已删除 - historyId={}", historyId);
+                return false;
+            }
+            
+            // 更新PDF URL
+            LambdaUpdateWrapper<StoreOperationalStatisticsHistory> wrapper = new LambdaUpdateWrapper<>();
+            wrapper.eq(StoreOperationalStatisticsHistory::getId, historyId)
+                   .eq(StoreOperationalStatisticsHistory::getDeleteFlag, 0)
+                   .set(StoreOperationalStatisticsHistory::getPdfUrl, pdfUrl.trim());
+            
+            int result = statisticsHistoryMapper.update(null, wrapper);
+            if (result > 0) {
+                log.info("更新历史统计记录PDF URL成功 - historyId={}, pdfUrl={}", historyId, pdfUrl);
+                return true;
+            } else {
+                log.warn("更新历史统计记录PDF URL失败,未更新任何记录 - historyId={}", historyId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("更新历史统计记录PDF URL失败 - historyId={}, pdfUrl={}, error={}", historyId, pdfUrl, e.getMessage(), e);
+            return false;
+        }
+    }
     
     // ==================== 累加和转换方法 ====================
     

+ 20 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java

@@ -419,6 +419,26 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
             dto.setAttachmentUrls(new ArrayList<>());
         }
 
+        // 填充商铺信息和商户头像
+        if (requirement.getStoreId() != null) {
+            // 查询商铺信息
+            StoreInfo storeInfo = storeInfoMapper.selectById(requirement.getStoreId());
+            if (storeInfo != null) {
+                dto.setStoreName(storeInfo.getStoreName());
+                dto.setStoreTel(storeInfo.getStoreTel());
+                dto.setStoreAddress(storeInfo.getStoreAddress());
+                dto.setStoreBlurb(storeInfo.getStoreBlurb());
+            }
+
+            // 查询商户头像(从store_user表获取head_img)
+            StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getStoreId, requirement.getStoreId())
+                    .eq(StoreUser::getDeleteFlag, 0));
+            if (storeUser != null && StringUtils.hasText(storeUser.getHeadImg())) {
+                dto.setStoreAvatar(storeUser.getHeadImg());
+            }
+        }
+
         return dto;
     }
 

+ 33 - 0
alien-store/src/main/java/shop/alien/store/util/FileUploadUtil.java

@@ -45,6 +45,7 @@ public class FileUploadUtil {
     List<String> privacyFileType = Arrays.asList("htm","html");
     List<String> ohterFileType = Arrays.asList("xls","xlsx");
     List<String> appFileType = Arrays.asList("apk", "ipk", "wgt");
+    List<String> pdfFileType = Arrays.asList("pdf");
 
     /**
      * 上传文件
@@ -60,6 +61,8 @@ public class FileUploadUtil {
                 prefix = "image/";
             } else if (videoFileType.contains(fileNameAndType.get("type").toLowerCase())) {
                 prefix = "video/";
+            } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                prefix = "pdf/";
             }
             // 去除文件名中的逗号,避免URL拼接时被错误分割
             String fileName = fileNameAndType.get("name").replaceAll(",", "");
@@ -92,6 +95,29 @@ public class FileUploadUtil {
     }
 
     /**
+     * 上传PDF文件
+     *
+     * @param multipartFile 文件名
+     * @return 文件路径
+     */
+    public String uploadPdf(MultipartFile multipartFile) {
+        try {
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
+            if (!pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                log.error("FileUpload.uploadPdf ERROR 该文件不是PDF格式文件");
+                return null;
+            }
+            String prefix = "pdf/";
+            // 去除文件名中的逗号,避免URL拼接时被错误分割
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            return aliOSSUtil.uploadFile(multipartFile, prefix + fileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type"));
+        } catch (Exception e) {
+            log.error("FileUpload.uploadPdf ERROR Msg={}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
      * 上传多个文件
      *
      * @param multipartRequest 多文件
@@ -155,6 +181,13 @@ public class FileUploadUtil {
                     // 去除文件名中的逗号,避免URL拼接时被错误分割
                     String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
                     filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
+                } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                    uploadDir += "/pdf";
+                    prefix = "pdf/";
+                    log.info("FileUpload.uploadMoreFile 获取到PDF文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
+                    // 去除文件名中的逗号,避免URL拼接时被错误分割
+                    String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
+                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
                 } else if (ohterFileType.contains(fileNameAndType.get("type").toLowerCase())) {
                     uploadDir += "/other/";
                     prefix = "other/";

+ 242 - 0
alien-store/src/main/java/shop/alien/store/util/ImageToPdfUploadUtil.java

@@ -0,0 +1,242 @@
+package shop.alien.store.util;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.util.ali.AliOSSUtil;
+import shop.alien.util.common.RandomCreateUtil;
+import shop.alien.util.file.FileUtil;
+import shop.alien.util.pdf.ImageToPdfUtil;
+
+import org.springframework.util.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 图片转PDF并上传工具类
+ * 接收前端上传的图片,转换为PDF后上传到OSS,返回PDF的线上URL
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+@Slf4j
+@Component
+@RefreshScope
+@RequiredArgsConstructor
+public class ImageToPdfUploadUtil {
+
+    private final AliOSSUtil aliOSSUtil;
+
+    /**
+     * 支持的图片格式
+     */
+    private static final List<String> SUPPORTED_IMAGE_TYPES = Arrays.asList("jpg", "jpeg", "png", "bmp", "webp", "gif");
+
+    /**
+     * 将单张图片转换为PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFile 图片文件
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imageToPdfAndUpload(MultipartFile imageFile) {
+        if (imageFile == null || imageFile.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 图片文件为空");
+            return null;
+        }
+
+        try {
+            // 验证文件类型
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 不支持的图片格式: {}", fileType);
+                return null;
+            }
+
+            // 直接从输入流读取图片并转换为PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageFile.getInputStream());
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            String ossFilePath = "pdf/" + fileName + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFiles 图片文件列表
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imagesToPdfAndUpload(List<MultipartFile> imageFiles) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 图片文件列表为空");
+            return null;
+        }
+
+        try {
+            // 收集所有有效的图片输入流
+            List<java.io.InputStream> imageStreams = new ArrayList<>();
+            for (MultipartFile imageFile : imageFiles) {
+                if (imageFile == null || imageFile.isEmpty()) {
+                    continue;
+                }
+
+                // 验证文件类型
+                Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+                String fileType = fileNameAndType.get("type").toLowerCase();
+                if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                    log.warn("ImageToPdfUploadUtil.imagesToPdfAndUpload WARN: 跳过不支持的图片格式: {}", fileType);
+                    continue;
+                }
+
+                // 直接使用输入流(内存方式)
+                imageStreams.add(imageFile.getInputStream());
+            }
+
+            if (imageStreams.isEmpty()) {
+                log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 没有有效的图片文件");
+                return null;
+            }
+
+            // 将多张图片合并为一个PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imagesToPdfBytes(imageStreams);
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String ossFilePath = "pdf/merged_" + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面)并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFile 图片文件
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imageToPdfA4AndUpload(MultipartFile imageFile) {
+        if (imageFile == null || imageFile.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 图片文件为空");
+            return null;
+        }
+
+        try {
+            // 验证文件类型
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 不支持的图片格式: {}", fileType);
+                return null;
+            }
+
+            // 直接从输入流读取图片并转换为PDF字节数组(A4页面,内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfA4Bytes(imageFile.getInputStream());
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            String ossFilePath = "pdf/" + fileName + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将Base64编码的图片转换为PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param base64Image Base64编码的图片字符串(支持带或不带data:image前缀)
+     * @return PDF的线上URL,失败返回null
+     */
+    public String base64ToPdfAndUpload(String base64Image) {
+        if (!StringUtils.hasText(base64Image)) {
+            log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64图片字符串为空");
+            return null;
+        }
+
+        try {
+            // 处理Base64字符串:移除可能的前缀(如 data:image/png;base64,)
+            String base64Data = base64Image.trim();
+            if (base64Data.contains(",")) {
+                base64Data = base64Data.substring(base64Data.indexOf(",") + 1);
+            }
+
+            // 解码Base64为字节数组
+            byte[] imageBytes;
+            try {
+                imageBytes = Base64.getDecoder().decode(base64Data);
+            } catch (IllegalArgumentException e) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64解码失败: {}", e.getMessage());
+                return null;
+            }
+
+            if (imageBytes == null || imageBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64解码后图片数据为空");
+                return null;
+            }
+
+            // 将字节数组转换为输入流
+            ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes);
+
+            // 转换为PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageInputStream);
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String ossFilePath = "pdf/base64_" + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+}

+ 63 - 0
alien-util/src/main/java/shop/alien/util/ali/AliOSSUtil.java

@@ -15,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile;
 import shop.alien.util.file.FileUtil;
 
 import java.io.File;
+import java.io.InputStream;
 
 /**
  * 阿里云oss工具类
@@ -136,4 +137,66 @@ public class AliOSSUtil {
         }
     }
 
+    /**
+     * oss上传文件(从输入流)
+     *
+     * @param inputStream 输入流
+     * @param ossFilePath oss中文件全路径(image/xxx.jpg)
+     * @return filePath
+     */
+    public String uploadFile(InputStream inputStream, String ossFilePath) {
+        // 验证参数
+        if (inputStream == null) {
+            log.error("AliOSSUtil.uploadFile ERROR: inputStream is null");
+            return null;
+        }
+        
+        if (StringUtils.isEmpty(ossFilePath)) {
+            log.error("AliOSSUtil.uploadFile ERROR: ossFilePath is empty");
+            return null;
+        }
+        
+        // 验证ossFilePath格式 - 不能以/开头
+        if (ossFilePath.startsWith("/")) {
+            log.error("AliOSSUtil.uploadFile ERROR: ossFilePath cannot start with '/': {}", ossFilePath);
+            return null;
+        }
+        
+        if (ossFilePath.contains("..") || ossFilePath.contains("\\")) {
+            log.error("AliOSSUtil.uploadFile ERROR: invalid ossFilePath format: {}", ossFilePath);
+            return null;
+        }
+
+        // 创建OSSClient实例
+        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
+        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
+        OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
+        try {
+            // 创建PutObjectRequest对象(使用输入流)
+            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, ossFilePath, inputStream);
+            // 上传文件
+            PutObjectResult result = ossClient.putObject(putObjectRequest);
+            String eTag = result.getETag();
+            if (StringUtils.isNotEmpty(eTag)) {
+                return "https://" + bucketName + "." + endPoint + "/" + ossFilePath;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("AliOSSUtil.uploadFile ERROR: {}", e.getMessage(), e);
+            return null;
+        } finally {
+            // 关闭输入流
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (Exception e) {
+                    log.warn("AliOSSUtil.uploadFile WARN: 关闭输入流失败: {}", e.getMessage());
+                }
+            }
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
 }

+ 682 - 0
alien-util/src/main/java/shop/alien/util/pdf/ImageToPdfUtil.java

@@ -0,0 +1,682 @@
+package shop.alien.util.pdf;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 图片转PDF工具类
+ * 支持单张图片转PDF、多张图片合并为一个PDF
+ * 支持从本地文件、URL、字节数组转换
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+public class ImageToPdfUtil {
+
+    /**
+     * 将单张图片文件转换为PDF
+     *
+     * @param imagePath 图片文件路径
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(String imagePath, String pdfPath) {
+        return imageToPdf(new File(imagePath), new File(pdfPath));
+    }
+
+    /**
+     * 将单张图片文件转换为PDF
+     *
+     * @param imageFile 图片文件
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(File imageFile, File pdfFile) {
+        if (imageFile == null || !imageFile.exists()) {
+            throw new IllegalArgumentException("图片文件不存在: " + (imageFile != null ? imageFile.getPath() : "null"));
+        }
+
+        try (InputStream imageStream = new FileInputStream(imageFile)) {
+            return imageToPdf(imageStream, pdfFile);
+        } catch (IOException e) {
+            throw new RuntimeException("读取图片文件失败: " + imageFile.getPath(), e);
+        }
+    }
+
+    /**
+     * 将图片输入流转换为PDF
+     *
+     * @param imageStream 图片输入流
+     * @param pdfFile     PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(InputStream imageStream, File pdfFile) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建PDF页面,使用图片的尺寸
+            PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+            document.addPage(page);
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片URL转换为PDF
+     *
+     * @param imageUrl 图片URL
+     * @param pdfPath  PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlToPdf(String imageUrl, String pdfPath) {
+        return imageUrlToPdf(imageUrl, new File(pdfPath));
+    }
+
+    /**
+     * 将图片URL转换为PDF
+     *
+     * @param imageUrl 图片URL
+     * @param pdfFile  PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlToPdf(String imageUrl, File pdfFile) {
+        if (imageUrl == null || imageUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("图片URL不能为空");
+        }
+
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(10000); // 10秒连接超时
+            connection.setReadTimeout(30000);    // 30秒读取超时
+            connection.setRequestMethod("GET");
+            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+            InputStream imageStream = connection.getInputStream();
+            try {
+                return imageToPdf(imageStream, pdfFile);
+            } finally {
+                if (imageStream != null) {
+                    imageStream.close();
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("从URL下载图片失败: " + imageUrl, e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 将图片字节数组转换为PDF
+     *
+     * @param imageBytes 图片字节数组
+     * @param pdfPath    PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageBytesToPdf(byte[] imageBytes, String pdfPath) {
+        return imageBytesToPdf(imageBytes, new File(pdfPath));
+    }
+
+    /**
+     * 将图片字节数组转换为PDF
+     *
+     * @param imageBytes 图片字节数组
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageBytesToPdf(byte[] imageBytes, File pdfFile) {
+        if (imageBytes == null || imageBytes.length == 0) {
+            throw new IllegalArgumentException("图片字节数组不能为空");
+        }
+
+        ByteArrayInputStream imageStream = new ByteArrayInputStream(imageBytes);
+        try {
+            return imageToPdf(imageStream, pdfFile);
+        } finally {
+            try {
+                imageStream.close();
+            } catch (IOException e) {
+                // ByteArrayInputStream的close()实际上不会抛出异常,但为了编译通过需要捕获
+            }
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF
+     *
+     * @param imagePaths 图片文件路径列表
+     * @param pdfPath    PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdf(List<String> imagePaths, String pdfPath) {
+        return imagesToPdfFromPaths(imagePaths, new File(pdfPath));
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(从文件路径)
+     *
+     * @param imagePaths 图片文件路径列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfFromPaths(List<String> imagePaths, File pdfFile) {
+        if (imagePaths == null || imagePaths.isEmpty()) {
+            throw new IllegalArgumentException("图片路径列表不能为空");
+        }
+
+        List<File> imageFiles = new ArrayList<>();
+        for (String imagePath : imagePaths) {
+            imageFiles.add(new File(imagePath));
+        }
+        return imagesToPdfFromFiles(imageFiles, pdfFile);
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(从文件对象)
+     *
+     * @param imageFiles 图片文件列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfFromFiles(List<File> imageFiles, File pdfFile) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            throw new IllegalArgumentException("图片文件列表不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            for (File imageFile : imageFiles) {
+                if (imageFile == null || !imageFile.exists()) {
+                    continue; // 跳过不存在的文件
+                }
+
+                try (FileInputStream imageStream = new FileInputStream(imageFile)) {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建PDF页面,使用图片的尺寸
+                    PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                    document.addPage(page);
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片失败: " + imageFile.getPath() + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片URL合并为一个PDF
+     *
+     * @param imageUrls 图片URL列表
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlsToPdf(List<String> imageUrls, String pdfPath) {
+        return imageUrlsToPdf(imageUrls, new File(pdfPath));
+    }
+
+    /**
+     * 将多张图片URL合并为一个PDF
+     *
+     * @param imageUrls 图片URL列表
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlsToPdf(List<String> imageUrls, File pdfFile) {
+        if (imageUrls == null || imageUrls.isEmpty()) {
+            throw new IllegalArgumentException("图片URL列表不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            for (String imageUrl : imageUrls) {
+                if (imageUrl == null || imageUrl.trim().isEmpty()) {
+                    continue; // 跳过空的URL
+                }
+
+                try {
+                    URL url = new URL(imageUrl);
+                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+                    connection.setConnectTimeout(10000);
+                    connection.setReadTimeout(30000);
+                    connection.setRequestMethod("GET");
+                    connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+                    InputStream imageStream = connection.getInputStream();
+                    try {
+                        BufferedImage bufferedImage = ImageIO.read(imageStream);
+                        if (bufferedImage == null) {
+                            continue; // 跳过无法读取的图片
+                        }
+
+                        // 创建PDF页面,使用图片的尺寸
+                        PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                        document.addPage(page);
+
+                        // 将图片添加到PDF页面
+                        PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                                document, imageToByteArray(bufferedImage), "image");
+
+                        try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                            contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                        }
+                    } finally {
+                        if (imageStream != null) {
+                            imageStream.close();
+                        }
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片URL失败: " + imageUrl + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片URL转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片转换为字节数组
+     *
+     * @param bufferedImage 图片对象
+     * @return 图片字节数组
+     */
+    private static byte[] imageToByteArray(BufferedImage bufferedImage) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        String format = "png"; // 默认使用PNG格式
+        ImageIO.write(bufferedImage, format, baos);
+        return baos.toByteArray();
+    }
+
+    /**
+     * 将图片输入流转换为PDF字节数组(内存方式,不保存文件)
+     *
+     * @param imageStream 图片输入流
+     * @return PDF字节数组
+     */
+    public static byte[] imageToPdfBytes(InputStream imageStream) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建PDF页面,使用图片的尺寸
+            PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+            document.addPage(page);
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片输入流转换为PDF字节数组(A4页面大小,内存方式)
+     *
+     * @param imageStream 图片输入流
+     * @return PDF字节数组
+     */
+    public static byte[] imageToPdfA4Bytes(InputStream imageStream) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建A4页面
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+
+            // 计算图片在A4页面上的尺寸(保持宽高比)
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+            float imageWidth = bufferedImage.getWidth();
+            float imageHeight = bufferedImage.getHeight();
+
+            // 计算缩放比例,使图片适应A4页面
+            float scaleX = pageWidth / imageWidth;
+            float scaleY = pageHeight / imageHeight;
+            float scale = Math.min(scaleX, scaleY);
+
+            float scaledWidth = imageWidth * scale;
+            float scaledHeight = imageHeight * scale;
+
+            // 居中显示
+            float x = (pageWidth - scaledWidth) / 2;
+            float y = (pageHeight - scaledHeight) / 2;
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面大小,图片自适应)
+     *
+     * @param imagePath 图片文件路径
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdfA4(String imagePath, String pdfPath) {
+        return imageToPdfA4(new File(imagePath), new File(pdfPath));
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面大小,图片自适应)
+     *
+     * @param imageFile 图片文件
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdfA4(File imageFile, File pdfFile) {
+        if (imageFile == null || !imageFile.exists()) {
+            throw new IllegalArgumentException("图片文件不存在: " + (imageFile != null ? imageFile.getPath() : "null"));
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            BufferedImage bufferedImage = ImageIO.read(imageFile);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建A4页面
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+
+            // 计算图片在A4页面上的尺寸(保持宽高比)
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+            float imageWidth = bufferedImage.getWidth();
+            float imageHeight = bufferedImage.getHeight();
+
+            // 计算缩放比例,使图片适应A4页面
+            float scaleX = pageWidth / imageWidth;
+            float scaleY = pageHeight / imageHeight;
+            float scale = Math.min(scaleX, scaleY);
+
+            float scaledWidth = imageWidth * scale;
+            float scaledHeight = imageHeight * scale;
+
+            // 居中显示
+            float x = (pageWidth - scaledWidth) / 2;
+            float y = (pageHeight - scaledHeight) / 2;
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(A4页面大小,每张图片一页,图片自适应)
+     *
+     * @param imageFiles 图片文件列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfA4(List<File> imageFiles, File pdfFile) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            throw new IllegalArgumentException("图片文件列表不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+
+            for (File imageFile : imageFiles) {
+                if (imageFile == null || !imageFile.exists()) {
+                    continue; // 跳过不存在的文件
+                }
+
+                try (FileInputStream imageStream = new FileInputStream(imageFile)) {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建A4页面
+                    PDPage page = new PDPage(PDRectangle.A4);
+                    document.addPage(page);
+
+                    // 计算图片在A4页面上的尺寸(保持宽高比)
+                    float imageWidth = bufferedImage.getWidth();
+                    float imageHeight = bufferedImage.getHeight();
+
+                    // 计算缩放比例,使图片适应A4页面
+                    float scaleX = pageWidth / imageWidth;
+                    float scaleY = pageHeight / imageHeight;
+                    float scale = Math.min(scaleX, scaleY);
+
+                    float scaledWidth = imageWidth * scale;
+                    float scaledHeight = imageHeight * scale;
+
+                    // 居中显示
+                    float x = (pageWidth - scaledWidth) / 2;
+                    float y = (pageHeight - scaledHeight) / 2;
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片失败: " + imageFile.getPath() + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片输入流合并为一个PDF字节数组(内存方式,不保存文件)
+     *
+     * @param imageStreams 图片输入流列表
+     * @return PDF字节数组
+     */
+    public static byte[] imagesToPdfBytes(List<InputStream> imageStreams) {
+        if (imageStreams == null || imageStreams.isEmpty()) {
+            throw new IllegalArgumentException("图片输入流列表不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            
+            for (InputStream imageStream : imageStreams) {
+                if (imageStream == null) {
+                    continue; // 跳过空的输入流
+                }
+
+                try {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建PDF页面,使用图片的尺寸
+                    PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                    document.addPage(page);
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片输入流失败: " + e.getMessage());
+                } finally {
+                    // 关闭输入流
+                    if (imageStream != null) {
+                        try {
+                            imageStream.close();
+                        } catch (IOException e) {
+                            // 忽略关闭异常
+                        }
+                    }
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+}