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.List; import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @Slf4j @Service 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<>(); try { if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } entity.setTenantId(entity.getTenantId().trim()); if (entity.getUserPhone() != null) { entity.setUserPhone(entity.getUserPhone().trim()); } if (entity.getUserName() != null) { entity.setUserName(entity.getUserName().trim()); } if (entity.getDikouAmt() == null) { entity.setDikouAmt(BigDecimal.ZERO); } if (entity.getId() == null || entity.getId().trim().isEmpty()) { entity.setId(UUID.randomUUID().toString()); } LocalDateTime now = LocalDateTime.now(); if (entity.getCreateTime() == null) { entity.setCreateTime(now); } entity.setUpdateTime(now); boolean ok = this.save(entity); result.put("success", ok); result.put("message", ok ? "新增成功" : "新增失败"); if (ok) { result.put("data", entity); } return result; } catch (Exception e) { log.error("抵扣金记录新增异常", e); result.put("success", false); result.put("message", "新增异常:" + e.getMessage()); return result; } } @Override public Map update(LbDeductionAmount entity) { Map result = new HashMap<>(); try { if (entity.getId() == null || entity.getId().trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } if (entity.getTenantId() != null) { entity.setTenantId(entity.getTenantId().trim()); } if (entity.getUserPhone() != null) { entity.setUserPhone(entity.getUserPhone().trim()); } if (entity.getUserName() != null) { entity.setUserName(entity.getUserName().trim()); } entity.setUpdateTime(LocalDateTime.now()); boolean ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "编辑成功" : "编辑失败"); if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { log.error("抵扣金记录编辑异常", e); result.put("success", false); result.put("message", "编辑异常:" + e.getMessage()); return result; } } @Override public Map deleteById(String id) { Map result = new HashMap<>(); try { if (id == null || id.trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } boolean ok = this.removeById(id.trim()); result.put("success", ok); result.put("message", ok ? "删除成功" : "删除失败"); return result; } catch (Exception e) { log.error("抵扣金记录删除异常", e); result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return result; } } @Override public Map pageQuery(Integer current, Integer size, String tenantId, String userName, String userPhone, String createTimeStart, String createTimeEnd) { Map result = new HashMap<>(); try { if (current == null || current < 1) { current = 1; } if (size == null || size < 1) { size = 10; } LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); if (tenantId != null && !tenantId.trim().isEmpty()) { queryWrapper.eq(LbDeductionAmount::getTenantId, tenantId.trim()); } if (userName != null && !userName.trim().isEmpty()) { queryWrapper.like(LbDeductionAmount::getUserName, userName.trim()); } if (userPhone != null && !userPhone.trim().isEmpty()) { queryWrapper.like(LbDeductionAmount::getUserPhone, userPhone.trim()); } LocalDateTime start = parseDateTime(createTimeStart); if (createTimeStart != null && !createTimeStart.trim().isEmpty() && start == null) { result.put("success", false); result.put("message", "createTimeStart 格式错误,请使用 yyyy-MM-dd HH:mm:ss"); return result; } LocalDateTime end = parseDateTime(createTimeEnd); if (createTimeEnd != null && !createTimeEnd.trim().isEmpty() && end == null) { result.put("success", false); result.put("message", "createTimeEnd 格式错误,请使用 yyyy-MM-dd HH:mm:ss"); return result; } if (start != null) { queryWrapper.ge(LbDeductionAmount::getCreateTime, start); } if (end != null) { queryWrapper.le(LbDeductionAmount::getCreateTime, end); } queryWrapper.orderByDesc(LbDeductionAmount::getUpdateTime) .orderByDesc(LbDeductionAmount::getCreateTime); Page page = this.page(new Page<>(current, size), queryWrapper); result.put("success", true); result.put("message", "查询成功"); result.put("data", page.getRecords()); result.put("total", page.getTotal()); result.put("current", page.getCurrent()); result.put("size", page.getSize()); result.put("pages", page.getPages()); return result; } catch (Exception e) { log.error("抵扣金记录查询异常", e); result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } @Override public Map sumDikouAmtByUserNameForReportDate(String tenantId, LocalDate reportDate) { Map byUserName = new HashMap<>(); if (tenantId == null || tenantId.trim().isEmpty() || reportDate == null) { return byUserName; } LocalDateTime start = reportDate.atStartOfDay(); LocalDateTime end = reportDate.plusDays(1).atStartOfDay(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LbDeductionAmount::getTenantId, tenantId.trim()) .ge(LbDeductionAmount::getCreateTime, start) .lt(LbDeductionAmount::getCreateTime, end); List records = list(queryWrapper); for (LbDeductionAmount record : records) { if (record.getUserName() == null || record.getUserName().trim().isEmpty()) { continue; } String key = record.getUserName().trim(); BigDecimal amt = record.getDikouAmt() == null ? BigDecimal.ZERO : record.getDikouAmt(); byUserName.merge(key, amt, BigDecimal::add); } return byUserName; } private static LocalDateTime parseDateTime(String text) { if (text == null || text.trim().isEmpty()) { return null; } try { return LocalDateTime.parse(text.trim(), DATE_TIME_FORMATTER); } catch (Exception e) { return null; } } }