506 lines
21 KiB
Java
506 lines
21 KiB
Java
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<LbPurchaseApplyMapper, LbPurchaseApply>
|
||
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<String, Object> parseFromOrderInfoByLlm(String orderInfo, String tenantId) {
|
||
Map<String, Object> 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<String, Object> add(LbPurchaseApply entity) {
|
||
Map<String, Object> 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<LbPurchaseApply> 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<String, Object> update(LbPurchaseApply entity) {
|
||
Map<String, Object> 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<String, Object> deleteById(Long id) {
|
||
Map<String, Object> 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<String, Object> pageQuery(Integer current,
|
||
Integer size,
|
||
String tenantId,
|
||
String applyUser,
|
||
String applyPhone,
|
||
String colleagueName,
|
||
String teamLeader,
|
||
String teamBigLeader,
|
||
LocalDate applyDate) {
|
||
Map<String, Object> result = new HashMap<>();
|
||
try {
|
||
if (current == null || current < 1) {
|
||
current = 1;
|
||
}
|
||
if (size == null || size < 1) {
|
||
size = 10;
|
||
}
|
||
|
||
LambdaQueryWrapper<LbPurchaseApply> 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<LbPurchaseApply> 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<String, Object> listNeedPrivilegeRecommenders(LocalDate startDate, LocalDate endDate) {
|
||
Map<String, Object> 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<LbPurchaseApply> purchaseQuery = new LambdaQueryWrapper<>();
|
||
purchaseQuery.ge(LbPurchaseApply::getApplyDate, startDate)
|
||
.le(LbPurchaseApply::getApplyDate, endDate)
|
||
.orderByDesc(LbPurchaseApply::getApplyDate)
|
||
.orderByDesc(LbPurchaseApply::getApplyDate) ;
|
||
List<LbPurchaseApply> 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<String> 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<LbDailyUserTradeReport> reportQuery = new LambdaQueryWrapper<>();
|
||
reportQuery.in(LbDailyUserTradeReport::getNickname, applyUsers)
|
||
.select(LbDailyUserTradeReport::getNickname);
|
||
List<LbDailyUserTradeReport> matchedReports = lbDailyUserTradeReportMapper.selectList(reportQuery);
|
||
|
||
Set<String> matchedNicknames = new HashSet<>();
|
||
for (LbDailyUserTradeReport report : matchedReports) {
|
||
if (report.getNickname() != null && !report.getNickname().trim().isEmpty()) {
|
||
matchedNicknames.add(report.getNickname().trim());
|
||
}
|
||
}
|
||
|
||
List<LbPurchaseApply> 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;
|
||
}
|
||
}
|
||
}
|