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.LbDailyUserTradeReport; import com.rj.entity.LbPurchaseApply; import com.rj.util.IsoWorkdayUtils; import com.rj.mapper.LbDailyUserTradeReportMapper; import com.rj.mapper.LbPurchaseApplyMapper; import com.rj.service.ILbPurchaseApplyService; 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.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @Slf4j @Service public class LbPurchaseApplyServiceImpl extends ServiceImpl implements ILbPurchaseApplyService { @Autowired private LbDailyUserTradeReportMapper lbDailyUserTradeReportMapper; @Autowired private ObjectMapper objectMapper; @Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}") private String dashScopeChatModel; private static final DateTimeFormatter TRY_DATE_MM_DD = DateTimeFormatter.ofPattern("MM.dd"); /** 试用期包含的工作日天数(周一至周五),周六日不计入。 */ private static final int TRIAL_WEEKDAY_COUNT = 3; private static final String PARSE_SYSTEM_PROMPT = """ 你是信息抽取助手。用户会提供一段「进货申请」相关的自然语言订货信息。 请只输出一个 JSON 对象,不要 markdown 代码块,不要解释性文字。 字段均为可选;无法从原文推断的字段请省略或设为 null。 tryDate 试用日期:若同时给出 applyDate,服务端会按 3 个工作日规则重算并覆盖本字段;模型可省略 tryDate。 字段名与含义(JSON 键名必须完全一致): applyUser 申请人姓名; applyPhone 申请人电话; tryDate 试用日期(可省略;有 applyDate 时由服务端按 3 个工作日重算); age 年龄(整数); workExperience 从业经历; colleagueName 同事姓名; colleaguePhone 同事电话; teamLeader 团队长; teamBigLeader 双收益团队长; applyDate 申请日期,格式 yyyy-MM-dd,如果年的信息为空,请用系统的当前年替换。 不要输出 id、tenantId、createTime、updateTime。 """; @Override public Map parseFromOrderInfoByLlm(String orderInfo, String tenantId) { Map result = new HashMap<>(); try { if (orderInfo == null || orderInfo.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; } 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" + orderInfo.trim()) .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); LbPurchaseApply entity = objectMapper.readValue(json, LbPurchaseApply.class); log.debug( "parseFromOrderInfoByLlm:LLM JSON 解析后 applyDate={} tryDate={}", entity.getApplyDate(), entity.getTryDate()); entity.setTenantId(tenantId.trim()); entity.setId(null); entity.setCreateTime(null); entity.setUpdateTime(null); fillTryDateFromApplyDate(entity); result.put("success", true); result.put("message", "解析成功"); result.put("data", entity); return result; } 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; } } /** * 试用日期:只要有 {@code applyDate},一律按服务端规则计算(覆盖模型可能错填的 tryDate)。 * 试用期为连续 {@link #TRIAL_WEEKDAY_COUNT} 个工作日(周一至周五),周六日为无效日不计入。 * 展示起始为申请日 applyDate(日历日),结束日为从「申请日及之后首个工作日」起计的第 3 个工作日(含该首日)。 * 若申请日年份不是当年,先按 {@link LocalDate#withYear(int)} 校正为当年同月同日(闰年 2 月 29 日在平年变为 2 月 28 日)。 */ private void fillTryDateFromApplyDate(LbPurchaseApply entity) { if (entity == null) { return; } LocalDate applyDate = entity.getApplyDate(); if (applyDate == null) { log.info("试用日期:applyDate 为空,不覆盖,保留 LLM tryDate={}", entity.getTryDate()); return; } int currentYear = LocalDate.now().getYear(); if (applyDate.getYear() != currentYear) { applyDate = applyDate.withYear(currentYear); entity.setApplyDate(applyDate); } String existingTryDate = entity.getTryDate(); LocalDate trialFirstWeekday = firstWeekdayOnOrAfter(applyDate); LocalDate endDate = endOfInclusiveWeekdaySpan(trialFirstWeekday, TRIAL_WEEKDAY_COUNT); String computed = applyDate.format(TRY_DATE_MM_DD) + " 到 " + endDate.format(TRY_DATE_MM_DD); entity.setTryDate(computed); } /** 申请日及之后第一个周一至周五(申请日本身为工作日时即为当天)。 */ private static LocalDate firstWeekdayOnOrAfter(LocalDate d) { LocalDate x = d; while (IsoWorkdayUtils.isWeekend(x)) { x = x.plusDays(1); } return x; } /** * 从 {@code firstWeekday} 起连续 {@code inclusiveWeekdays} 个工作日(仅周一至周五,含起点当日),返回该段最后一天。 * 若起点落在周六日,会先顺延到下一工作日再计数,且返回值保证不为周六日。 */ private LocalDate endOfInclusiveWeekdaySpan(LocalDate firstWeekday, int inclusiveWeekdays) { if (inclusiveWeekdays < 1) { throw new IllegalArgumentException("inclusiveWeekdays must be >= 1"); } LocalDate d = firstWeekday; while (IsoWorkdayUtils.isWeekend(d)) { if (log.isDebugEnabled()) { log.debug( "trialSpan:起点为周末,顺延 cursor={} dow={} isoDow={}", d, d.getDayOfWeek(), IsoWorkdayUtils.isoDayOfWeek(d)); } d = d.plusDays(1); } int weekdaysRemaining = inclusiveWeekdays; int guard = 0; while (true) { boolean weekend = IsoWorkdayUtils.isWeekend(d); if (log.isDebugEnabled()) { log.debug( "trialSpan:cursor={} dow={} isoDow={} weekend={} weekdaysRemaining={}", d, d.getDayOfWeek(), IsoWorkdayUtils.isoDayOfWeek(d), weekend, weekdaysRemaining); } if (!weekend) { weekdaysRemaining--; if (weekdaysRemaining == 0) { return d; } } d = d.plusDays(1); guard++; if (guard > 400) { throw new IllegalStateException( "trialSpan:超过 400 步仍未结束,firstWeekday=" + firstWeekday + " inclusiveWeekdays=" + inclusiveWeekdays); } } } 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(LbPurchaseApply entity) { Map result = new HashMap<>(); try { if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } if (entity.getApplyUser() == null || entity.getApplyUser().trim().isEmpty()) { result.put("success", false); result.put("message", "applyUser不能为空"); return result; } if (entity.getApplyPhone() == null || entity.getApplyPhone().trim().isEmpty()) { result.put("success", false); result.put("message", "applyPhone不能为空"); return result; } if (entity.getApplyDate() == null) { result.put("success", false); result.put("message", "applyDate不能为空"); return result; } String tenantId = entity.getTenantId().trim(); String phone = entity.getApplyPhone().trim(); entity.setTenantId(tenantId); entity.setApplyPhone(phone); LocalDateTime now = LocalDateTime.now(); LambdaQueryWrapper phoneQuery = new LambdaQueryWrapper<>(); phoneQuery.eq(LbPurchaseApply::getTenantId, tenantId) .eq(LbPurchaseApply::getApplyPhone, phone) .orderByDesc(LbPurchaseApply::getId) .last("LIMIT 1"); LbPurchaseApply existing = this.getOne(phoneQuery); boolean ok; if (existing != null) { entity.setId(existing.getId()); entity.setCreateTime(existing.getCreateTime()); entity.setUpdateTime(now); ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "按申请人电话已更新" : "更新失败"); } else { entity.setId(null); if (entity.getCreateTime() == null) { entity.setCreateTime(now); } entity.setUpdateTime(now); ok = this.save(entity); result.put("success", ok); result.put("message", ok ? "新增成功" : "新增失败"); } if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { result.put("success", false); result.put("message", "新增异常:" + e.getMessage()); return result; } } @Override public Map update(LbPurchaseApply entity) { Map result = new HashMap<>(); try { if (entity.getId() == null) { result.put("success", false); result.put("message", "id不能为空"); return result; } 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) { result.put("success", false); result.put("message", "编辑异常:" + e.getMessage()); return result; } } @Override public Map deleteById(Long id) { Map result = new HashMap<>(); try { boolean ok = this.removeById(id); result.put("success", ok); result.put("message", ok ? "删除成功" : "删除失败"); return result; } catch (Exception e) { result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return result; } } @Override public Map pageQuery(Integer current, Integer size, String tenantId, String applyUser, String applyPhone, String colleagueName, String teamLeader, String teamBigLeader, LocalDate applyDate) { 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(LbPurchaseApply::getTenantId, tenantId.trim()); } if (applyUser != null && !applyUser.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getApplyUser, applyUser.trim()); } if (applyPhone != null && !applyPhone.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getApplyPhone, applyPhone.trim()); } if (colleagueName != null && !colleagueName.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getColleagueName, colleagueName.trim()); } if (teamLeader != null && !teamLeader.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getTeamLeader, teamLeader.trim()); } if (teamBigLeader != null && !teamBigLeader.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getTeamBigLeader, teamBigLeader.trim()); } if (applyDate != null) { queryWrapper.eq(LbPurchaseApply::getApplyDate, applyDate); } queryWrapper.orderByDesc(LbPurchaseApply::getUpdateTime) .orderByDesc(LbPurchaseApply::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) { result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } @Override public Map listNeedPrivilegeRecommenders(LocalDate startDate, LocalDate endDate) { Map result = new HashMap<>(); try { if (startDate == null || endDate == null) { result.put("success", false); result.put("message", "开始日期和结束日期不能为空"); return result; } if (endDate.isBefore(startDate)) { result.put("success", false); result.put("message", "结束日期不能早于开始日期"); return result; } LambdaQueryWrapper purchaseQuery = new LambdaQueryWrapper<>(); purchaseQuery.ge(LbPurchaseApply::getApplyDate, startDate) .le(LbPurchaseApply::getApplyDate, endDate) .orderByDesc(LbPurchaseApply::getApplyDate) .orderByDesc(LbPurchaseApply::getApplyDate) ; List purchaseInfo = this.list(purchaseQuery); if (purchaseInfo == null || purchaseInfo.isEmpty()) { result.put("success", true); result.put("message", "查询成功"); result.put("data", List.of()); result.put("total", 0); return result; } Set applyUsers = new HashSet<>(); for (LbPurchaseApply item : purchaseInfo) { if (item.getApplyUser() != null && !item.getApplyUser().trim().isEmpty()) { applyUsers.add(item.getApplyUser().trim()); } } if (applyUsers.isEmpty()) { result.put("success", true); result.put("message", "查询成功"); result.put("data", List.of()); result.put("total", 0); return result; } LambdaQueryWrapper reportQuery = new LambdaQueryWrapper<>(); reportQuery.in(LbDailyUserTradeReport::getNickname, applyUsers) .select(LbDailyUserTradeReport::getNickname); List matchedReports = lbDailyUserTradeReportMapper.selectList(reportQuery); Set matchedNicknames = new HashSet<>(); for (LbDailyUserTradeReport report : matchedReports) { if (report.getNickname() != null && !report.getNickname().trim().isEmpty()) { matchedNicknames.add(report.getNickname().trim()); } } List needPrivilegeList = new ArrayList<>(); for (LbPurchaseApply item : purchaseInfo) { String applyUser = item.getApplyUser() == null ? null : item.getApplyUser().trim(); if (applyUser != null && !applyUser.isEmpty() && matchedNicknames.contains(applyUser)) { needPrivilegeList.add(item); } } result.put("success", true); result.put("message", "查询成功"); result.put("data", needPrivilegeList); result.put("total", needPrivilegeList.size()); result.put("startDate", startDate); result.put("endDate", endDate); return result; } catch (Exception e) { result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } }