|
|
@@ -1,28 +1,41 @@
|
|
|
-/*
|
|
|
- * Copyright (c) 2018-2025, jackzhou All rights reserved.
|
|
|
- *
|
|
|
- * Redistribution and use in source and binary forms, with or without
|
|
|
- * modification, are permitted provided that the following conditions are met:
|
|
|
- *
|
|
|
- * Redistributions of source code must retain the above copyright notice,
|
|
|
- * this list of conditions and the following disclaimer.
|
|
|
- * Redistributions in binary form must reproduce the above copyright
|
|
|
- * notice, this list of conditions and the following disclaimer in the
|
|
|
- * documentation and/or other materials provided with the distribution.
|
|
|
- * Neither the name of the jfcloud.vip developer nor the names of its
|
|
|
- * contributors may be used to endorse or promote products derived from
|
|
|
- * this software without specific prior written permission.
|
|
|
- * Author: jackzhou (i_amzxj@163.com)
|
|
|
- */
|
|
|
package com.github.jfcloud.aigc.mineralanalysistask.service.impl;
|
|
|
|
|
|
+import cn.hutool.core.io.FileUtil;
|
|
|
+import cn.hutool.core.io.file.FileNameUtil;
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import cn.hutool.json.JSONArray;
|
|
|
+import cn.hutool.json.JSONObject;
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
import com.github.jfcloud.aigc.mineralanalysistask.dto.MineralAnalysisTaskDTO;
|
|
|
import com.github.jfcloud.aigc.mineralanalysistask.entity.MineralAnalysisTask;
|
|
|
import com.github.jfcloud.aigc.mineralanalysistask.mapper.MineralAnalysisTaskMapper;
|
|
|
import com.github.jfcloud.aigc.mineralanalysistask.service.MineralAnalysisTaskService;
|
|
|
+import com.github.jfcloud.aigc.mineraldetectiondetail.entity.MineralDetectionDetail;
|
|
|
+import com.github.jfcloud.aigc.mineraldetectiondetail.service.MineralDetectionDetailService;
|
|
|
import com.github.jfcloud.common.data.service.impl.JfcloudBaseServiceImpl;
|
|
|
+import com.github.jfcloud.common.file.core.FileTemplate;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.core.io.FileSystemResource;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.LinkedMultiValueMap;
|
|
|
+import org.springframework.util.MultiValueMap;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
|
|
|
/**
|
|
|
* 矿石分析记录表
|
|
|
@@ -30,7 +43,294 @@ import org.springframework.stereotype.Service;
|
|
|
* @author jackzhou
|
|
|
* @date 2025-12-01 16:07:52
|
|
|
*/
|
|
|
+@Slf4j
|
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
|
public class MineralAnalysisTaskServiceImpl extends JfcloudBaseServiceImpl<MineralAnalysisTaskMapper, MineralAnalysisTaskDTO, MineralAnalysisTask> implements MineralAnalysisTaskService {
|
|
|
-}
|
|
|
+ private final RestTemplate restTemplate;
|
|
|
+ private final MineralDetectionDetailService detailService;
|
|
|
+ private final FileTemplate fileTemplate;
|
|
|
+
|
|
|
+ //从YML读取Python服务配置
|
|
|
+ @Value("${python.api.base-url}")
|
|
|
+ private String pythonBaseUrl;
|
|
|
+
|
|
|
+ @Value("${python.api.image-endpoint}")
|
|
|
+ private String imageEndpoint;
|
|
|
+
|
|
|
+ @Value("${file.upload.bucket:aigc}")
|
|
|
+ private String uploadBucket;
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public MineralAnalysisTask analyzeImage(MultipartFile file, String modelVersion, BigDecimal confThreshold, String precisionMode) {
|
|
|
+
|
|
|
+ //初始化任务记录
|
|
|
+ String taskCode = "TASK-" + IdUtil.getSnowflakeNextIdStr();
|
|
|
+ MineralAnalysisTask task = new MineralAnalysisTask();
|
|
|
+ task.setTaskCode(taskCode);
|
|
|
+ task.setModelVersion(modelVersion);
|
|
|
+ task.setConfThreshold(confThreshold);
|
|
|
+ task.setPrecisionMode(precisionMode);
|
|
|
+ task.setStatus("0"); // 0-进行中
|
|
|
+
|
|
|
+
|
|
|
+ String originalFileName = file.getOriginalFilename();
|
|
|
+ String localFileName = IdUtil.simpleUUID() + "." + FileNameUtil.getSuffix(originalFileName);
|
|
|
+
|
|
|
+ File localFile = new File(System.getProperty("user.dir"), localFileName);
|
|
|
+ try {
|
|
|
+ file.transferTo(localFile);
|
|
|
+ fileTemplate.putObject(uploadBucket, localFileName, Files.newInputStream(localFile.toPath()));
|
|
|
+ task.setImage(String.format("/admin/sys-file/%s/%s", uploadBucket, localFileName));
|
|
|
+ //先保存一次任务,获取ID
|
|
|
+ this.save(task);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("文件保存失败", e);
|
|
|
+ throw new RuntimeException("文件保存失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ //构建请求头
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
|
+
|
|
|
+ //构建请求体
|
|
|
+ MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
|
|
+ body.add("file", new FileSystemResource(localFile));
|
|
|
+
|
|
|
+ //传参给Python
|
|
|
+ body.add("modelVersion", modelVersion); // 传模型版本 (1, 2, 3)
|
|
|
+ body.add("confThreshold", confThreshold); // 传置信度
|
|
|
+ body.add("precisionMode", precisionMode); // 传精度模式 (standard/high)
|
|
|
+
|
|
|
+ HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
|
|
+
|
|
|
+ //拼接完整的URL
|
|
|
+ String fullUrl = pythonBaseUrl + imageEndpoint;
|
|
|
+ log.info("调用 Python 接口: {}, 参数: model={}, conf={}, precision={}", fullUrl, modelVersion, confThreshold, precisionMode);
|
|
|
+
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(fullUrl, requestEntity, String.class);
|
|
|
+
|
|
|
+ if (!response.getStatusCode().is2xxSuccessful()) {
|
|
|
+ throw new RuntimeException("Python接口返回错误码: " + response.getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ //解析Python返回的JSON
|
|
|
+ String respBody = response.getBody();
|
|
|
+ log.info("Python 返回结果: {}", respBody);
|
|
|
+
|
|
|
+ JSONObject jsonRes = JSONUtil.parseObj(respBody);
|
|
|
+ JSONArray resultsArray = jsonRes.getJSONArray("results");
|
|
|
+
|
|
|
+ if (resultsArray == null || resultsArray.isEmpty()) {
|
|
|
+ throw new RuntimeException("未识别到任何结果");
|
|
|
+ }
|
|
|
+
|
|
|
+ //取第一个结果
|
|
|
+ JSONObject firstResult = resultsArray.getJSONObject(0);
|
|
|
+ String resultUrl = firstResult.getStr("saved_path"); // /results/pred_0_xxx.jpg
|
|
|
+ JSONArray classes = firstResult.getJSONArray("classes");
|
|
|
+ JSONArray confidences = firstResult.getJSONArray("confidence");
|
|
|
+
|
|
|
+ //更新主任务表 (MineralAnalysisTask)
|
|
|
+ //Python返回的是绝对路径
|
|
|
+ File resultFile = new File(resultUrl);
|
|
|
+ if (!resultFile.exists()) {
|
|
|
+ throw new RuntimeException("识别文件不存在" + resultUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ String resultFileName = IdUtil.simpleUUID() + "." + FileNameUtil.getSuffix(resultFile);
|
|
|
+ fileTemplate.putObject(uploadBucket, resultFileName, Files.newInputStream(resultFile.toPath()));
|
|
|
+ task.setResultImage(String.format("/admin/sys-file/%s/%s", uploadBucket, resultFileName));
|
|
|
+
|
|
|
+ //将["石英", "方解石"] 转为 "石英, 方解石"存入summary
|
|
|
+ List<String> classList = classes.toList(String.class);
|
|
|
+ String summary = String.join(",", classList);
|
|
|
+ task.setAiSummary(summary);
|
|
|
+
|
|
|
+ task.setStatus("1"); // 1-成功
|
|
|
+ task.setFinishTime(LocalDateTime.now());
|
|
|
+
|
|
|
+ this.updateById(task);
|
|
|
+ //插入详情表(MineralDetectionDetail)
|
|
|
+ //循环索引匹配class和confidence
|
|
|
+ List<MineralDetectionDetail> details = new ArrayList<>();
|
|
|
+
|
|
|
+ //循环索引匹配class和confidence
|
|
|
+ if (classes != null && !classes.isEmpty()) {
|
|
|
+
|
|
|
+ for (int i = 0; i < classes.size(); i++) {
|
|
|
+ MineralDetectionDetail detail = new MineralDetectionDetail();
|
|
|
+ detail.setTaskId(task.getId());
|
|
|
+
|
|
|
+ // 获取当前索引的矿石名称
|
|
|
+ String mineralType = classes.getStr(i);
|
|
|
+ detail.setMineralType(mineralType);
|
|
|
+
|
|
|
+ //根据识别名称自动生成写死的模拟数据
|
|
|
+ switch (mineralType) {
|
|
|
+ case "蓝铜矿": // Azurite
|
|
|
+ detail.setMineralContent("蓝铜矿85%、孔雀石10%、褐铁矿5%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("粗粒");
|
|
|
+ detail.setMineralStructure("短柱状或板状晶体");
|
|
|
+ detail.setCollectionLocation("安徽省·池州市·六峰山");
|
|
|
+ break;
|
|
|
+ case "重晶石": // Baryte
|
|
|
+ detail.setMineralContent("硫酸钡96%、锶2%、钙1%、微量杂质1%");
|
|
|
+ detail.setWeatheringDegree("II 轻度风化");
|
|
|
+ detail.setGrainSize("中粗粒");
|
|
|
+ detail.setMineralStructure("板状晶簇或块状");
|
|
|
+ detail.setCollectionLocation("贵州省·天柱县·大河边重晶石矿");
|
|
|
+ break;
|
|
|
+ case "绿柱石": // Beryl
|
|
|
+ detail.setMineralContent("绿柱石92%、长石5%、石英3%");
|
|
|
+ detail.setWeatheringDegree("0 未风化");
|
|
|
+ detail.setGrainSize("巨晶");
|
|
|
+ detail.setMineralStructure("六方柱状晶体");
|
|
|
+ detail.setCollectionLocation("新疆·阿勒泰地区·可可托海");
|
|
|
+ break;
|
|
|
+ case "方解石": // Calcite
|
|
|
+ detail.setMineralContent("方解石98%、白云石1%、黄铁矿1%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("粗粒");
|
|
|
+ detail.setMineralStructure("菱面体晶簇");
|
|
|
+ detail.setCollectionLocation("湖南省·郴州市·香花岭");
|
|
|
+ break;
|
|
|
+ case "白铅矿": // Cerussite
|
|
|
+ detail.setMineralContent("白铅矿80%、方铅矿10%、褐铁矿10%");
|
|
|
+ detail.setWeatheringDegree("III 中度风化");
|
|
|
+ detail.setGrainSize("细粒");
|
|
|
+ detail.setMineralStructure("致密块状或网状");
|
|
|
+ detail.setCollectionLocation("云南省·贡山县·铅锌矿带");
|
|
|
+ break;
|
|
|
+ case "铜": // Copper (自然铜)
|
|
|
+ case "自然铜":
|
|
|
+ detail.setMineralContent("自然铜95%、赤铜矿3%、孔雀石2%");
|
|
|
+ detail.setWeatheringDegree("II 轻度风化");
|
|
|
+ detail.setGrainSize("细粒");
|
|
|
+ detail.setMineralStructure("树枝状或不规则块状");
|
|
|
+ detail.setCollectionLocation("湖北省·大冶市·铜绿山");
|
|
|
+ break;
|
|
|
+ case "萤石": // Fluorite
|
|
|
+ detail.setMineralContent("氟化钙95%、石英3%、重晶石2%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("粗粒");
|
|
|
+ detail.setMineralStructure("立方体晶簇");
|
|
|
+ detail.setCollectionLocation("浙江省·武义县·杨家矿区");
|
|
|
+ break;
|
|
|
+ case "石膏": // Gypsum
|
|
|
+ detail.setMineralContent("二水石膏96%、硬石膏2%、粘土矿物2%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("纤维状");
|
|
|
+ detail.setMineralStructure("纤维状集合体");
|
|
|
+ detail.setCollectionLocation("湖北省·应城市·石膏矿");
|
|
|
+ break;
|
|
|
+ case "赤铁矿": // Hematite
|
|
|
+ detail.setMineralContent("赤铁矿90%、磁铁矿5%、石英5%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("隐晶质");
|
|
|
+ detail.setMineralStructure("肾状或鲕状构造");
|
|
|
+ detail.setCollectionLocation("河北省·张家口市·宣化");
|
|
|
+ break;
|
|
|
+ case "孔雀石": // Malachite
|
|
|
+ detail.setMineralContent("孔雀石95%、赤铜矿3%、褐铁矿2%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("隐晶质");
|
|
|
+ detail.setMineralStructure("葡萄状或钟乳状结构");
|
|
|
+ detail.setCollectionLocation("广东省·阳春市·石录铜矿");
|
|
|
+ break;
|
|
|
+ case "黄铁矿": // Pyrite
|
|
|
+ detail.setMineralContent("黄铁矿90%、石英8%、其他硫化物2%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("中粗粒");
|
|
|
+ detail.setMineralStructure("立方体或五角十二面体");
|
|
|
+ detail.setCollectionLocation("安徽省·马鞍山市·南山矿");
|
|
|
+ break;
|
|
|
+ case "磷氯铅矿": // Pyromorphite
|
|
|
+ detail.setMineralContent("磷氯铅矿92%、褐铁矿5%、石英3%");
|
|
|
+ detail.setWeatheringDegree("0 未风化");
|
|
|
+ detail.setGrainSize("细粒");
|
|
|
+ detail.setMineralStructure("六方柱状晶簇");
|
|
|
+ detail.setCollectionLocation("广西·桂林市·道坪铅锌矿");
|
|
|
+ break;
|
|
|
+ case "石英": // Quartz
|
|
|
+ detail.setMineralContent("二氧化硅99.5%、微量杂质0.5%");
|
|
|
+ detail.setWeatheringDegree("0 未风化");
|
|
|
+ detail.setGrainSize("粗粒");
|
|
|
+ detail.setMineralStructure("晶质结构");
|
|
|
+ detail.setCollectionLocation("江苏省·东海县·水晶乡");
|
|
|
+ break;
|
|
|
+ case "菱锌矿": // Smithsonite
|
|
|
+ detail.setMineralContent("菱锌矿85%、褐铁矿10%、方解石5%");
|
|
|
+ detail.setWeatheringDegree("II 轻度风化");
|
|
|
+ detail.setGrainSize("隐晶质");
|
|
|
+ detail.setMineralStructure("葡萄状集合体");
|
|
|
+ detail.setCollectionLocation("云南省·兰坪县·金顶铅锌矿");
|
|
|
+ break;
|
|
|
+ case "钼铅矿": // Wulfenite
|
|
|
+ detail.setMineralContent("钼铅矿90%、方解石5%、褐铁矿5%");
|
|
|
+ detail.setWeatheringDegree("I 微风化");
|
|
|
+ detail.setGrainSize("粗粒");
|
|
|
+ detail.setMineralStructure("薄板状晶体");
|
|
|
+ detail.setCollectionLocation("新疆·巴州·库鲁克塔格");
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ // 默认值 (或识别出未知矿石时使用)
|
|
|
+ detail.setMineralContent("未知矿物成分");
|
|
|
+ detail.setWeatheringDegree("未知");
|
|
|
+ detail.setGrainSize("未知");
|
|
|
+ detail.setMineralStructure("不规则块状");
|
|
|
+ detail.setCollectionLocation("待定区域");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ //获取当前索引的置信度
|
|
|
+ //判断数组长度防止越界
|
|
|
+ if (confidences != null && i < confidences.size()) {
|
|
|
+ String confStr = confidences.getStr(i);
|
|
|
+ detail.setConfidence(new BigDecimal(confStr));
|
|
|
+ } else {
|
|
|
+ detail.setConfidence(confThreshold);
|
|
|
+ }
|
|
|
+
|
|
|
+ details.add(detail);
|
|
|
+ }
|
|
|
+ // 批量保存到数据库
|
|
|
+ detailService.saveBatch(details);
|
|
|
+ }else {
|
|
|
+ // 【新增逻辑】如果识别结果为空,生成一条默认的“未识别”记录,防止前端报错并展示默认信息
|
|
|
+ MineralDetectionDetail detail = new MineralDetectionDetail();
|
|
|
+ detail.setTaskId(task.getId());
|
|
|
+ detail.setMineralType("未识别");
|
|
|
+
|
|
|
+ // 使用你指定的默认值
|
|
|
+ detail.setMineralContent("未知矿物成分");
|
|
|
+ detail.setWeatheringDegree("未知");
|
|
|
+ detail.setGrainSize("未知");
|
|
|
+ detail.setMineralStructure("不规则块状");
|
|
|
+ detail.setCollectionLocation("待定区域");
|
|
|
+
|
|
|
+ // 置信度设为0,表示未检测到
|
|
|
+ detail.setConfidence(BigDecimal.ZERO);
|
|
|
+
|
|
|
+ details.add(detail);
|
|
|
+ //保存到数据库
|
|
|
+ detailService.saveBatch(details);
|
|
|
+ }
|
|
|
+ if (details != null && !details.isEmpty()) {
|
|
|
+ task.setDetails(details.get(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ return task;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("AI分析过程异常", e);
|
|
|
+ task.setStatus("2"); // 2-失败
|
|
|
+ task.setErrorMsg(e.getMessage());
|
|
|
+ this.updateById(task);
|
|
|
+ throw new RuntimeException("分析失败: " + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ FileUtil.del(localFile);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|