From 26592d357cbb60b62be241597f5caba8ceb1e739 Mon Sep 17 00:00:00 2001 From: cst61 Date: Thu, 21 May 2026 11:58:01 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=8A=B5=E6=89=A3=E9=87=91?= =?UTF-8?q?=E8=87=AA=E7=84=B6=E8=AF=AD=E8=A8=80=E4=B8=BA=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LbDeductionAmountController.java | 32 +++ .../rj/service/ILbDeductionAmountService.java | 5 + .../impl/LbDeductionAmountServiceImpl.java | 238 ++++++++++++++++++ 3 files changed, 275 insertions(+) diff --git a/src/main/java/com/rj/controller/LbDeductionAmountController.java b/src/main/java/com/rj/controller/LbDeductionAmountController.java index bf1d439..d42890f 100644 --- a/src/main/java/com/rj/controller/LbDeductionAmountController.java +++ b/src/main/java/com/rj/controller/LbDeductionAmountController.java @@ -4,7 +4,11 @@ import com.rj.entity.LbDeductionAmount; import com.rj.service.ILbDeductionAmountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -19,6 +23,31 @@ public class LbDeductionAmountController { @Autowired private ILbDeductionAmountService lbDeductionAmountService; + @Data + @Schema(description = "大模型解析抵扣金信息请求体") + public static class ParseDeductionTextRequest { + @Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "tenantId不能为空") + private String tenantId; + + @Schema(description = "抵扣金相关自然语言原文", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "文本不能为空") + private String text; + } + + @PostMapping("/parseFromText") + @Operation(summary = "大模型解析抵扣金文本并保存", description = "根据自然语言调用大模型抽取字段为 LbDeductionAmount,落库后返回") + public ResponseEntity> parseFromText( + @Parameter(description = "租户ID与自然语言文本", required = true) @Valid @RequestBody ParseDeductionTextRequest body) { + Map result = lbDeductionAmountService.parseFromTextByLlmAndSave( + body.getTenantId(), body.getText()); + Boolean success = (Boolean) result.get("success"); + if (success != null && success) { + return ResponseEntity.ok(result); + } + return ResponseEntity.badRequest().body(result); + } + @PostMapping("/add") @Operation(summary = "新增") public ResponseEntity> add( @@ -77,4 +106,7 @@ public class LbDeductionAmountController { } return ResponseEntity.internalServerError().body(result); } + + + } diff --git a/src/main/java/com/rj/service/ILbDeductionAmountService.java b/src/main/java/com/rj/service/ILbDeductionAmountService.java index 3b7a680..49c033c 100644 --- a/src/main/java/com/rj/service/ILbDeductionAmountService.java +++ b/src/main/java/com/rj/service/ILbDeductionAmountService.java @@ -20,4 +20,9 @@ public interface ILbDeductionAmountService extends IService { String userPhone, String createTimeStart, String createTimeEnd); + + /** + * 根据自然语言文本调用大模型解析为 {@link LbDeductionAmount} 并落库。 + */ + Map parseFromTextByLlmAndSave(String tenantId, String text); } diff --git a/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java b/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java index 0c731b8..9f9a691 100644 --- a/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java @@ -1,20 +1,34 @@ package com.rj.service.impl; +import com.alibaba.dashscope.aigc.generation.Generation; +import com.alibaba.dashscope.aigc.generation.GenerationParam; +import com.alibaba.dashscope.aigc.generation.GenerationResult; +import com.alibaba.dashscope.common.Message; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.exception.NoApiKeyException; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.ObjectMapper; import com.rj.entity.LbDeductionAmount; import com.rj.mapper.LbDeductionAmountMapper; import com.rj.service.ILbDeductionAmountService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Slf4j @Service @@ -22,9 +36,233 @@ public class LbDeductionAmountServiceImpl extends ServiceImpl implements ILbDeductionAmountService { + @Autowired + private ObjectMapper objectMapper; + + @Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}") + private String dashScopeChatModel; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /** 常见正文:首行「M月d日」,次行「姓名 手机号」,含「申请…抵扣…金额」与「已提现金额」。 */ + private static final Pattern MONTH_DAY_PATTERN = Pattern.compile("(\\d{1,2})月(\\d{1,2})日"); + private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("1[3-9]\\d{9}"); + /** 优先匹配「申请…抵扣…金额数字」,避免误用「已提现金额」。 */ + private static final Pattern DIKOU_AMT_PATTERN = + Pattern.compile("申请[^\\n]*?抵扣[^\\n]*?金额\\s*(\\d+(?:\\.\\d+)?)", Pattern.CASE_INSENSITIVE); + + private static final String PARSE_SYSTEM_PROMPT = """ + 你是信息抽取助手。用户会提供「抵扣金记录」自然语言,常见为多行固定格式,例如: + 5月21日 + 刘兰兰 13261700877 + 申请FXJ抵扣SJF金额2678 + 已提现金额2678 + + 解析规则: + 1. 首行或文中「M月d日」(如 5月21日)仅作业务日期参考,不要写入 JSON(服务端会从原文补 createTime)。 + 2. 「姓名 + 空格 + 11位手机号」同一行:userName 为姓名,userPhone 为手机号。 + 3. dikouAmt 取含「申请」且含「抵扣」且含「金额」那一行中的数字(如上例 2678);不要用「已提现金额」作为 dikouAmt。 + 4. FXJ、SJF 等字母为业务代号,忽略即可。 + 5. 金额只输出数字,不要带「元」等单位。 + + 请只输出一个 JSON 对象,不要 markdown 代码块,不要解释性文字。 + 字段名(必须完全一致):userName、userPhone、dikouAmt。 + createTime 若填写,格式 yyyy-MM-dd HH:mm:ss;服务端仅将非当前年的年份校正为系统当前年,月日不变。 + 不要输出 id、tenantId、originalText、updateTime。 + """; + + @Override + public Map parseFromTextByLlmAndSave(String tenantId, String text) { + Map result = new HashMap<>(); + try { + if (text == null || text.trim().isEmpty()) { + result.put("success", false); + result.put("message", "文本不能为空"); + return result; + } + if (tenantId == null || tenantId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "tenantId不能为空"); + return result; + } + + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + result.put("success", false); + result.put("message", "未配置环境变量 DASHSCOPE_API_KEY,无法调用大模型"); + return result; + } + + String trimmedText = text.trim(); + Generation gen = new Generation(); + Message systemMsg = Message.builder() + .role(Role.SYSTEM.getValue()) + .content(PARSE_SYSTEM_PROMPT) + .build(); + Message userMsg = Message.builder() + .role(Role.USER.getValue()) + .content("抵扣金信息如下:\n" + trimmedText) + .build(); + + GenerationParam param = GenerationParam.builder() + .apiKey(apiKey) + .model(dashScopeChatModel) + .messages(Arrays.asList(systemMsg, userMsg)) + .resultFormat(GenerationParam.ResultFormat.MESSAGE) + .build(); + + GenerationResult call = gen.call(param); + String raw = call.getOutput().getChoices().get(0).getMessage().getContent(); + if (raw == null || raw.trim().isEmpty()) { + result.put("success", false); + result.put("message", "大模型返回内容为空"); + return result; + } + + String json = normalizeJson(raw); + LbDeductionAmount entity = objectMapper.readValue(json, LbDeductionAmount.class); + alignCreateTimeYearToCurrent(entity); + + entity.setTenantId(tenantId.trim()); + entity.setOriginalText(trimmedText); + entity.setId(null); + entity.setUpdateTime(null); + + enrichFromDeductionText(entity, trimmedText); + log.debug( + "parseFromTextByLlmAndSave:userName={} userPhone={} dikouAmt={} createTime={}", + entity.getUserName(), + entity.getUserPhone(), + entity.getDikouAmt(), + entity.getCreateTime()); + + return add(entity); + } catch (NoApiKeyException e) { + log.error("DashScope API Key 异常", e); + result.put("success", false); + result.put("message", "API密钥异常:" + e.getMessage()); + return result; + } catch (InputRequiredException e) { + log.error("DashScope 入参异常", e); + result.put("success", false); + result.put("message", "调用大模型入参错误:" + e.getMessage()); + return result; + } catch (Exception e) { + log.error("抵扣金信息大模型解析失败", e); + result.put("success", false); + result.put("message", "解析失败:" + e.getMessage()); + return result; + } + } + + /** + * 大模型若返回 createTime 且年份不是当前年,仅将年份改为系统当前年,月日及时分秒不变。 + */ + private void alignCreateTimeYearToCurrent(LbDeductionAmount entity) { + if (entity == null || entity.getCreateTime() == null) { + return; + } + int currentYear = LocalDate.now().getYear(); + LocalDate parsedDate = entity.getCreateTime().toLocalDate(); + if (parsedDate.getYear() == currentYear) { + return; + } + LocalDate adjustedDate = parsedDate.withYear(currentYear); + LocalDateTime adjusted = LocalDateTime.of(adjustedDate, entity.getCreateTime().toLocalTime()); + log.info("大模型返回年份 {} 与系统年份 {} 不一致,createTime 已调整为 {}", parsedDate.getYear(), currentYear, adjusted); + entity.setCreateTime(adjusted); + } + + /** + * 按业务常见多行格式补全 LLM 可能漏填的字段(日期、姓名电话、抵扣金额)。 + */ + private void enrichFromDeductionText(LbDeductionAmount entity, String text) { + if (entity == null || text == null || text.isBlank()) { + return; + } + fillCreateTimeFromText(entity, text); + // 含「申请…抵扣…金额」时以该行为准,避免模型误用「已提现金额」 + fillDikouAmtFromText(entity, text); + if (entity.getUserPhone() == null || entity.getUserPhone().isBlank()) { + fillPhoneFromText(entity, text); + } + if (entity.getUserName() == null || entity.getUserName().isBlank()) { + fillUserNameFromText(entity, text); + } + } + + private void fillCreateTimeFromText(LbDeductionAmount entity, String text) { + if (entity.getCreateTime() != null) { + return; + } + Matcher matcher = MONTH_DAY_PATTERN.matcher(text); + if (!matcher.find()) { + return; + } + int month = Integer.parseInt(matcher.group(1)); + int day = Integer.parseInt(matcher.group(2)); + LocalDate date = LocalDate.of(LocalDate.now().getYear(), month, day); + entity.setCreateTime(date.atStartOfDay()); + } + + private void fillDikouAmtFromText(LbDeductionAmount entity, String text) { + Matcher matcher = DIKOU_AMT_PATTERN.matcher(text); + if (matcher.find()) { + entity.setDikouAmt(new BigDecimal(matcher.group(1))); + return; + } + if (entity.getDikouAmt() == null || entity.getDikouAmt().signum() == 0) { + Matcher fallback = Pattern.compile("金额\\s*(\\d+(?:\\.\\d+)?)").matcher(text); + if (fallback.find()) { + entity.setDikouAmt(new BigDecimal(fallback.group(1))); + } + } + } + + private void fillPhoneFromText(LbDeductionAmount entity, String text) { + Matcher matcher = MOBILE_PHONE_PATTERN.matcher(text); + if (matcher.find()) { + entity.setUserPhone(matcher.group()); + } + } + + private void fillUserNameFromText(LbDeductionAmount entity, String text) { + if (entity.getUserPhone() == null || entity.getUserPhone().isBlank()) { + fillPhoneFromText(entity, text); + } + String phone = entity.getUserPhone(); + if (phone == null || phone.isBlank()) { + return; + } + for (String line : text.split("\\R")) { + String trimmedLine = line.trim(); + if (!trimmedLine.contains(phone)) { + continue; + } + String namePart = trimmedLine.replace(phone, "").trim(); + if (!namePart.isEmpty()) { + entity.setUserName(namePart); + return; + } + } + } + + private static String normalizeJson(String content) { + if (content == null) { + return ""; + } + String trimmed = content.trim(); + if (trimmed.startsWith("```")) { + int firstLineBreak = trimmed.indexOf('\n'); + int lastFence = trimmed.lastIndexOf("```"); + if (firstLineBreak >= 0 && lastFence > firstLineBreak) { + trimmed = trimmed.substring(firstLineBreak + 1, lastFence).trim(); + } + } + return trimmed; + } + @Override public Map add(LbDeductionAmount entity) { Map result = new HashMap<>();