解析抵扣金自然语言为实体类
This commit is contained in:
@@ -20,4 +20,9 @@ public interface ILbDeductionAmountService extends IService<LbDeductionAmount> {
|
||||
String userPhone,
|
||||
String createTimeStart,
|
||||
String createTimeEnd);
|
||||
|
||||
/**
|
||||
* 根据自然语言文本调用大模型解析为 {@link LbDeductionAmount} 并落库。
|
||||
*/
|
||||
Map<String, Object> parseFromTextByLlmAndSave(String tenantId, String text);
|
||||
}
|
||||
|
||||
@@ -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<LbDeductionAmountMapper, LbDeductionAmount>
|
||||
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<String, Object> parseFromTextByLlmAndSave(String tenantId, String text) {
|
||||
Map<String, Object> 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<String, Object> add(LbDeductionAmount entity) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
Reference in New Issue
Block a user