|
@@ -4,13 +4,27 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.etotem.cfc.entity.InsurancePolicy;
|
|
import com.etotem.cfc.entity.InsurancePolicy;
|
|
|
import com.etotem.cfc.mapper.InsurancePolicyMapper;
|
|
import com.etotem.cfc.mapper.InsurancePolicyMapper;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
import com.etotem.cfc.util.SortUtil;
|
|
import com.etotem.cfc.util.SortUtil;
|
|
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
@@ -22,6 +36,15 @@ public class InsurancePolicyService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private ReportSummaryService reportSummaryService;
|
|
private ReportSummaryService reportSummaryService;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private StorageService storageService;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${langgraph.base-url:http://localhost:9000}")
|
|
|
|
|
+ private String langgraphBaseUrl;
|
|
|
|
|
+
|
|
|
|
|
+ private final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
public List<InsurancePolicy> getPolicies(Long userId, String status) {
|
|
public List<InsurancePolicy> getPolicies(Long userId, String status) {
|
|
|
LambdaQueryWrapper<InsurancePolicy> wrapper = new LambdaQueryWrapper<InsurancePolicy>()
|
|
LambdaQueryWrapper<InsurancePolicy> wrapper = new LambdaQueryWrapper<InsurancePolicy>()
|
|
|
.eq(InsurancePolicy::getUserId, userId);
|
|
.eq(InsurancePolicy::getUserId, userId);
|
|
@@ -70,4 +93,161 @@ public class InsurancePolicyService {
|
|
|
log.warn("标记保单废弃失败 policyId={}", id, e);
|
|
log.warn("标记保单废弃失败 policyId={}", id, e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传保单PDF文件,调用LangGraph AI解析,返回提取的字段预览(不自动入库)。
|
|
|
|
|
+ * 前端拿到结果后展示给用户确认,用户修改后再调 create/update 入库。
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> uploadAndParse(MultipartFile file, Long userId, Long familyId) throws Exception {
|
|
|
|
|
+ // 1. 保存文件
|
|
|
|
|
+ String fileUrl = storageService.storeFile(file, "insurance-policies");
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 读取原始文件字节,用于提取原文条款(PDF全文文本)
|
|
|
|
|
+ byte[] fileBytes = file.getBytes();
|
|
|
|
|
+ File tempFile = File.createTempFile("policy_", "_" + file.getOriginalFilename());
|
|
|
|
|
+ Files.write(tempFile.toPath(), fileBytes);
|
|
|
|
|
+ tempFile.deleteOnExit();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 调用 LangGraph 通用报告解析端点
|
|
|
|
|
+ Map<String, Object> parsedResult = callLangGraphParse(tempFile.getAbsolutePath());
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 从解析结果中提取保单关键字段(如果有的话)
|
|
|
|
|
+ Map<String, Object> extracted = extractInsuranceFields(parsedResult);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 提取原始文本作为保单条款
|
|
|
|
|
+ String rawText = extractPdfText(fileBytes);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("fileUrl", fileUrl);
|
|
|
|
|
+ result.put("originalFilename", file.getOriginalFilename());
|
|
|
|
|
+ result.put("extracted", extracted);
|
|
|
|
|
+ result.put("rawText", rawText);
|
|
|
|
|
+ result.put("langGraphResult", parsedResult);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 调用 LangGraph 通用报告解析端点
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Object> callLangGraphParse(String filePath) throws Exception {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String url = langgraphBaseUrl + "/api/v1/report/parse";
|
|
|
|
|
+ Map<String, Object> body = new LinkedHashMap<>();
|
|
|
|
|
+ body.put("file_path", filePath);
|
|
|
|
|
+ body.put("user_id", null);
|
|
|
|
|
+ body.put("family_id", null);
|
|
|
|
|
+
|
|
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers);
|
|
|
|
|
+
|
|
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
|
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
|
|
+ Map<String, Object> respMap = objectMapper.readValue(response.getBody(), Map.class);
|
|
|
|
|
+ if (respMap != null && Integer.valueOf(200).equals(respMap.get("code"))) {
|
|
|
|
|
+ return (Map<String, Object>) respMap.get("data");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("LangGraph 解析保单失败,使用空结果: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return new LinkedHashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 LangGraph 解析结果中提取保单相关字段
|
|
|
|
|
+ */
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private Map<String, Object> extractInsuranceFields(Map<String, Object> lgResult) {
|
|
|
|
|
+ Map<String, Object> extracted = new LinkedHashMap<>();
|
|
|
|
|
+ if (lgResult == null) return extracted;
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试从 sections 或 overview 中提取信息
|
|
|
|
|
+ Object overview = lgResult.get("overview");
|
|
|
|
|
+ if (overview instanceof Map) {
|
|
|
|
|
+ Map<String, Object> ov = (Map<String, Object>) overview;
|
|
|
|
|
+ extracted.put("insuranceCompany", ov.get("insuranceCompany"));
|
|
|
|
|
+ extracted.put("policyType", ov.get("policyType"));
|
|
|
|
|
+ extracted.put("insuredPerson", ov.get("insuredPerson"));
|
|
|
|
|
+ extracted.put("policyNo", ov.get("policyNo"));
|
|
|
|
|
+ extracted.put("premium", ov.get("premium"));
|
|
|
|
|
+ extracted.put("sumInsured", ov.get("sumInsured"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试从 sections 提取
|
|
|
|
|
+ Object sections = lgResult.get("sections");
|
|
|
|
|
+ if (sections instanceof List) {
|
|
|
|
|
+ for (Object section : (List<?>) sections) {
|
|
|
|
|
+ if (section instanceof Map) {
|
|
|
|
|
+ Map<String, Object> s = (Map<String, Object>) section;
|
|
|
|
|
+ String title = String.valueOf(s.getOrDefault("title", ""));
|
|
|
|
|
+ Object content = s.get("items");
|
|
|
|
|
+ if (content instanceof List) {
|
|
|
|
|
+ for (Object item : (List<?>) content) {
|
|
|
|
|
+ if (item instanceof Map) {
|
|
|
|
|
+ Map<String, Object> i = (Map<String, Object>) item;
|
|
|
|
|
+ String name = String.valueOf(i.getOrDefault("name", ""));
|
|
|
|
|
+ String value = String.valueOf(i.getOrDefault("value", ""));
|
|
|
|
|
+ if (name.contains("保险") && !extracted.containsKey("insuranceCompany")) {
|
|
|
|
|
+ extracted.put("insuranceCompany", value);
|
|
|
|
|
+ } else if (name.contains("保单") && !extracted.containsKey("policyName")) {
|
|
|
|
|
+ extracted.put("policyName", value);
|
|
|
|
|
+ } else if (name.contains("保额") && !extracted.containsKey("sumInsured")) {
|
|
|
|
|
+ extracted.put("sumInsured", value);
|
|
|
|
|
+ } else if (name.contains("保费") && !extracted.containsKey("premium")) {
|
|
|
|
|
+ extracted.put("premium", value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 也检查 summary 内容
|
|
|
|
|
+ String textContent = String.valueOf(s.getOrDefault("content", ""));
|
|
|
|
|
+ if (!extracted.containsKey("summary") && textContent.length() > 10) {
|
|
|
|
|
+ extracted.put("summary", textContent.substring(0, Math.min(200, textContent.length())));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 兜底:从 indicators 中提取
|
|
|
|
|
+ Object indicators = lgResult.get("indicators");
|
|
|
|
|
+ if (indicators instanceof List) {
|
|
|
|
|
+ for (Object ind : (List<?>) indicators) {
|
|
|
|
|
+ if (ind instanceof Map) {
|
|
|
|
|
+ Map<String, Object> i = (Map<String, Object>) ind;
|
|
|
|
|
+ String name = String.valueOf(i.getOrDefault("name", ""));
|
|
|
|
|
+ String value = String.valueOf(i.getOrDefault("value", ""));
|
|
|
|
|
+ if (name.contains("保险公司") && !extracted.containsKey("insuranceCompany")) {
|
|
|
|
|
+ extracted.put("insuranceCompany", value);
|
|
|
|
|
+ } else if (name.contains("保单号") && !extracted.containsKey("policyNo")) {
|
|
|
|
|
+ extracted.put("policyNo", value);
|
|
|
|
|
+ } else if (name.contains("被保人") && !extracted.containsKey("insuredPerson")) {
|
|
|
|
|
+ extracted.put("insuredPerson", value);
|
|
|
|
|
+ } else if (name.contains("保额") && !extracted.containsKey("sumInsured")) {
|
|
|
|
|
+ extracted.put("sumInsured", value);
|
|
|
|
|
+ } else if (name.contains("保费") && !extracted.containsKey("premium")) {
|
|
|
|
|
+ extracted.put("premium", value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return extracted;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 PDF 文件字节中提取纯文本作为保单原始条款
|
|
|
|
|
+ */
|
|
|
|
|
+ private String extractPdfText(byte[] fileBytes) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.pdmodel.PDDocument.load(fileBytes);
|
|
|
|
|
+ org.apache.pdfbox.text.PDFTextStripper stripper = new org.apache.pdfbox.text.PDFTextStripper();
|
|
|
|
|
+ String text = stripper.getText(document);
|
|
|
|
|
+ document.close();
|
|
|
|
|
+ return text != null ? text.trim() : "";
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("提取PDF原文失败: {}", e.getMessage());
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|