pdf 转图片

前景提示 需要用ai去校验数据,但是ai接口只支持单个图片,但是用户可以上传pdf,所以需要pdf转图片

设计分析

  1. url判断是否是 pdf
    • 否 直接调用ai接口
    • 是 进行pdf下载
      • pdf转图片 上传阿里云 获取到 图片链接
        • 用图片链接调用ai接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
String cleanUrl = StrUtil.subBefore(imageUrl, "?", false); // 截掉 ? 后面
String ext = StrUtil.subAfter(cleanUrl, ".", true); // 取最后一个点后的部分
String result;
List<String> imagePaths = new ArrayList<>();
if ("pdf".equalsIgnoreCase(ext)) {
System.out.println("pdf");
String originalFilename = FilenameUtils.getName(cleanUrl);
String baseName = FilenameUtils.getBaseName(originalFilename);
URL url = new URL(cleanUrl);
String key = url.getPath();
// 去掉最前面的 '/'
if (key.startsWith("/")) {
key = key.substring(1);
}// 返回 /qualification/20250902/FILExxx.pdf
// 将pdf下载成IO流 调用转换方法,把 PDF 转成图片
try (InputStream pdfInputStream = ossUploadService.downLoadFile4Qua(key)) {
List<InputStream> imageStrems = PdfToImageConverter.convertPdfToImages(pdfInputStream);
for (int i = 0; i < imageStrems.size(); i++) {
String fileNameAndType = baseName + "_page_" + i + ".png";
String path = ossUploadService.uploadFileToOssQualification(imageStrems.get(i), fileNameAndType, 100L);
String previewUrl = fileUploadService.getPreviewUrlForQualification(path);
imagePaths.add(previewUrl);
}
}
} else if ("png".equalsIgnoreCase(ext) || "jpg".equalsIgnoreCase(ext) || "jpeg".equalsIgnoreCase(ext)) {
imagePaths.add(imageUrl);
}

pdf转图片方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static List<InputStream> convertPdfToImages(InputStream pdfInputStream) throws IOException {
List<InputStream> imageStreams = new ArrayList<>();
try (PDDocument document = PDDocument.load(pdfInputStream)) {
PDFRenderer renderer = new PDFRenderer(document);
int totalPages = document.getNumberOfPages();

for (int i = 0; i < totalPages; i++) {
if (i > 3){
break;
}
BufferedImage image = renderer.renderImageWithDPI(i, 200);
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, "png", os);
InputStream is = new ByteArrayInputStream(os.toByteArray());
imageStreams.add(is);
}
} catch (Exception e) {
e.printStackTrace();
}

return imageStreams;
}

webclient 异步调用ai接口

因为需要校验的数据很多,为了较短 的响应时间 所以采用异步的方式

  1. Mono 和 Flux 的定义
    它们来自 Project Reactor(Spring WebFlux 的响应式编程库):
  • Mono
    • 表示 一个异步计算的结果,这个结果要么是:恰好一个值(比如一个字符串,一个对象)或者没有值(empty)
  • Flux
    • 表示 一个异步的数据流,可能包含 0、1 或多个元素。

Mono

  1. Mono.just(…)
    • 表示创建一个 Mono,里面包着一个已经有的值
      1
      2
      Mono<String> mono = Mono.just("Hello");
      mono.subscribe(System.out::println); // 打印 Hello
  2. Tuples.of(field, data)
    • Spring Reactor 提供了一个 Tuples 工具类,可以创建 Tuple(二元组,三元组…)。
      • Tuple2表示两个元素。
      • Tuple3表示三个元素。
        1
        2
        3
        Tuple2<String, Integer> t = Tuples.of("age", 18);
        System.out.println(t.getT1()); // age
        System.out.println(t.getT2()); // 18

(阻塞 vs 响应式)

  1. 传统阻塞写法
    1
    2
    3
    4
    5
    6
    // 同步等待结果,直到 WebClient 完成请求
    String result = client.get()
    .uri("/hello")
    .retrieve()
    .bodyToMono(String.class) // Mono<String>
    .block(); // 阻塞等待,返回 String
  2. 响应式写法
    1
    2
    3
    4
    5
    6
    7
    8
    Mono<String> resultMono = client.get()
    .uri("/hello")
    .retrieve()
    .bodyToMono(String.class);
    //这里 .subscribe(...) 就像注册了一个“回调”,等结果异步到了才处理。
    resultMono.subscribe(data -> {
    System.out.println("收到结果: " + data);
    });

    最终写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 1) 用来保存每个异步任务(每个任务最终产出 Tuple2<field,data>)
    List<Mono<Tuple2<String, String>>> tasksWithField = new ArrayList<>();

    // 2) WebClient 只构建一次,复用连接池
    WebClient client = WebClient.builder()
    .baseUrl(BASE_URL)
    .build();

    for (String imagePath : imagePaths) {
    for (String field : fields) {
    String prompt = "解析出图片上的" + field + ",我只要答案,不要其他字";

    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("imageUrl", imagePath);
    requestBody.put("prompt", prompt);
    requestBody.put("responseFormat", "");
    requestBody.put("serviceProvider", 1);

    // 3) 每个任务:调用远程 AI 服务,解析响应,如果有有效 data 则返回 Tuple2(field,data),否则返回 empty
    Mono<Tuple2<String, String>> task = client.post()
    .uri(QUALIFICATIONS_AI_URI)
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(requestBody)
    .retrieve()
    .bodyToMono(JsonNode.class)
    .flatMap(response -> {
    String success = response.has("success") ? response.get("success").asText() : "false";
    String data = response.has("data") ? response.get("data").asText() : null;
    if ("true".equals(success) && data != null && !"无".equals(data)) {
    return Mono.just(Tuples.of(field, data)); // 带上 field 的有效结果
    }
    return Mono.empty(); // 无效 -> 不发出值
    })
    .onErrorResume(e -> Mono.empty()); // 网络/解析异常也转成 empty,避免流中断

    tasksWithField.add(task);
    }
    }

    // 4) 合并所有任务,收集成 Map<field,data>,再 block() 阻塞获取
    Map<String, String> resultMap = Flux.merge(tasksWithField)
    .collectMap(Tuple2::getT1, Tuple2::getT2)
    .block();

结果

1
2
3
4
5
6
7
8
9
{
"retStatus": "1",
"retMessage": "success",
"retData": {
"品牌": "无品牌",
"报告编号": "lhg040211"
},
"timestamp": 0
}

PageHelper

前景提示

在今天准备上线的日子,遇见的一个关于PageHelper的坑,以后要注意一下
PageHelper开启分页后会对下面的 sql就行分页,一开始我是放到了第二个SQL上面,但是这样并不合理,因为第一个SQL可能也会查出来很多的数据,这样对第二个分页就没有意义了,但是当我把 PageHelper.startPage(param.getPageNum(), param.getPageSize()) .setReasonable(false) ; 放到一个SQL上面时,skuList是有数据的,但是在最后一行使用 pageHelper 进行构建分页结果的时候 会构建失败 返回空数据

1
2
3
4
5
6
7
8
9
10
11
12
13
  //1. 根据精准查询 先查出来sku信息
List<ProductSkuModel> productSkuModels = this.myDAO.getSkuInfoUnIonBySkus(param);
List<String> skuNos = productSkuModels.stream().map(ProductSkuModel::getSkuNo).collect(Collectors.toList());
// List<String> auditSkuNos = auditProductSkuModel.stream().map(ProductSkuAuditModel::getSkuNo).collect(Collectors.toList());
//' skuNos.addAll(auditSkuNos);
skuInfoMap = productSkuModels.stream()
.collect(Collectors.toMap(ProductSkuModel::getSkuNo, x -> x, (a, b) -> a)); // 避免重复 key
//2 通过sku查询 单据信息
param.setSkuNos(skuNos);
PageHelper.startPage(param.getPageNum(), param.getPageSize()) .setReasonable(false) ;
skuList = this.myDAO.getSkuByParamWithSkuInfo(param);

Pager<PrepareRecordSkuVo> pager = Pager.build4Mybatis(skuList);
  • 原因:Pager.build4Mybatis 需要一个page对象,但是我的开启分页放到第一个SQL上面,第二个SQL查询出来的就是一个普通集合了,所以会构建失败
1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> Pager<T> build4Mybatis(List<T> list) {
Pager<T> pager = new Pager<T>();
if (list instanceof Page) {
Page<T> page = (Page)list;
pager.setPageShow(page.getPageSize());
pager.setNowPage(page.getPageNum());
pager.setTotalNum(Math.toIntExact(page.getTotal()));
pager.setResults(page.getResult());
return pager;
} else {
return pager;
}
}

总结

  • PageHelper.startPage 需要放到最上面的SQL,因为可能会小表驱动大表
  • Pager.build4Mybatis 需要一个page类型的参数

线上数据太多导致查询失败

前景提示

做了一个查询功能,中间设计了多个表的查询,在测试环境上数据比较少只有几千条数据,所以测试环境上一直测试的都没问题,结果到了上线的时候,发现线上查不出来数据,只有加索引的几个条件 查询会很快。在无条件查询的情况下 就会出现查询不出来数据的情况

解决

最后解决是 将加索引的几个搜索条件变成了必填条件,这样就会走索引了,这解决方案不是很好,毕竟是限制了用户,但是这是当时紧急情况下的最好的解决方案考虑了,
因为 搜索条件有很多,很多字段是没有索引的,例如创建时间,模糊查询名称,一个模糊查询走索引不慢。但是数据量会查出来两百万条数据。

总结

线上数据量太大,不加条件,会进行全表扫面 速度极慢,让用户必须输入一些条件查询,进行走索引,加快查询速度

MYSQL UPDATE 知识补充

在 UPDATE 中不能在子查询里再次引用同一张表。 例如:

1
2
UPDATE  product_collect set salePlats ="marketing_saleplat_swan" 
where uuid in(select uuid from product_collect where productType in("05","08","09"))
  • 为什么不可以呢?

    1. MySQL 的执行方式是“一边读一边写”
      • 而 MySQL 在执行 UPDATE 时是这样工作的:按顺序扫描表 → 找到符合条件的行 → 更新它 。但同时,你又要求 MySQL 在更新过程中,再去读同一张可能正在被更新的表。
    2. MySQL 的扫描和更新顺序会破坏子查询结果
      • 由于 UPDATE 会修改表内容,子查询读取的数据可能:
        • 被 UPDATE 刚刚修改过
        • 还没被更新
        • 扫描顺序变化
        • 甚至行可能被锁住
          这会导致执行结果 不确定、不稳定、不安全。
  • 解决方法:

  1. 用子查询包一层
    1
    2
    3
    4
    5
    6
    7
    8
    9
    UPDATE product_collect
    SET salePlats = 'marketing_saleplat_swan'
    WHERE uuid IN (
    SELECT uuid FROM (
    SELECT uuid
    FROM product_collect
    WHERE productType IN ('05','08','09')
    ) AS tmp
    );
    原因: 最里面的子查询 会形成一个临时表 tem
  2. 使用临时表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    -- 1. 创建临时表,保存需要更新的 skuNo
    CREATE TEMPORARY TABLE tmp AS
    SELECT uuid, 'marketing_saleplat_swan' AS newSalePlats
    FROM product_collect
    WHERE productType IN ('05', '08', '09');

    -- 2. 用 JOIN 更新
    UPDATE product_collect pc
    JOIN tmp t ON pc.uuid = t.uuid
    SET pc.salePlats = t.newSalePlats;

    -- 3. 更新完成后删除临时表
    DROP TEMPORARY TABLE tmp;