diff --git a/src/main/java/com/rj/controller/LbPurchaseApplyController.java b/src/main/java/com/rj/controller/LbPurchaseApplyController.java index 2dcf6f7..ada55a6 100644 --- a/src/main/java/com/rj/controller/LbPurchaseApplyController.java +++ b/src/main/java/com/rj/controller/LbPurchaseApplyController.java @@ -50,7 +50,7 @@ public class LbPurchaseApplyController { } @PostMapping("/add") - @Operation(summary = "新增") + @Operation(summary = "新增或按申请人电话更新", description = "同一租户下若 applyPhone 已存在则更新该条,否则新增") public ResponseEntity> add( @Parameter(description = "实体", required = true) @RequestBody LbPurchaseApply entity) { Map result = lbPurchaseApplyService.add(entity); diff --git a/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java b/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java index 604ecba..b87ecc6 100644 --- a/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java @@ -13,6 +13,7 @@ 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; @@ -26,6 +27,7 @@ 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; @@ -46,21 +48,27 @@ public class LbPurchaseApplyServiceImpl @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 试用日期(原文怎么说就怎么填字符串,如 yyyy-MM-dd 或中文描述),起始日期是:申请日期applyDate,结束日期:申请日期applyDate 加 2天,跳过周六日 ,比如: 05.14 到 05.16 ; + tryDate 试用日期(可省略;有 applyDate 时由服务端按 3 个工作日重算); age 年龄(整数); workExperience 从业经历; colleagueName 同事姓名; colleaguePhone 同事电话; teamLeader 团队长; teamBigLeader 双收益团队长; - applyDate 申请日期,格式 yyyy-MM-dd,若原文无明确日期可省略。 + applyDate 申请日期,格式 yyyy-MM-dd,如果年的信息为空,请用系统的当前年替换。 不要输出 id、tenantId、createTime、updateTime。 """; @@ -113,11 +121,18 @@ public class LbPurchaseApplyServiceImpl 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); @@ -140,6 +155,95 @@ public class LbPurchaseApplyServiceImpl } } + /** + * 试用日期:只要有 {@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 ""; @@ -180,17 +284,39 @@ public class LbPurchaseApplyServiceImpl return result; } - LocalDateTime now = LocalDateTime.now(); - if (entity.getCreateTime() == null) { - entity.setCreateTime(now); - } - entity.setUpdateTime(now); + String tenantId = entity.getTenantId().trim(); + String phone = entity.getApplyPhone().trim(); + entity.setTenantId(tenantId); + entity.setApplyPhone(phone); - boolean ok = this.save(entity); - result.put("success", ok); - result.put("message", ok ? "新增成功" : "新增失败"); + 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", entity); + result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { diff --git a/src/main/java/com/rj/util/IsoWorkdayUtils.java b/src/main/java/com/rj/util/IsoWorkdayUtils.java new file mode 100644 index 0000000..6bfcd1d --- /dev/null +++ b/src/main/java/com/rj/util/IsoWorkdayUtils.java @@ -0,0 +1,25 @@ +package com.rj.util; + +import java.time.LocalDate; +import java.time.temporal.WeekFields; + +/** + * ISO-8601 工作日判断(与 {@link LocalDate} 默认历法一致)。 + *

+ * 周末判定使用 {@link WeekFields#ISO} 的 day-of-week 数值:周一=1 … 周日=7,周六、周日为 6、7。 + * 与 {@link java.time.LocalDate#getDayOfWeek()} 结论一致;调试时可打印 {@link #isoDayOfWeek(LocalDate)} 便于核对年份。 + */ +public final class IsoWorkdayUtils { + + private IsoWorkdayUtils() {} + + /** ISO 星期字段数值:1=周一 … 7=周日。 */ + public static int isoDayOfWeek(LocalDate d) { + return d.get(WeekFields.ISO.dayOfWeek()); + } + + /** 周六、周日为 true(isoDow 为 6 或 7)。 */ + public static boolean isWeekend(LocalDate d) { + return isoDayOfWeek(d) >= 6; + } +} diff --git a/src/test/java/com/rj/util/IsoWorkdayUtilsTest.java b/src/test/java/com/rj/util/IsoWorkdayUtilsTest.java new file mode 100644 index 0000000..9ee9d3f --- /dev/null +++ b/src/test/java/com/rj/util/IsoWorkdayUtilsTest.java @@ -0,0 +1,29 @@ +package com.rj.util; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IsoWorkdayUtilsTest { + + @Test + void may162026_isSaturday_weekend() { + LocalDate d = LocalDate.of(2026, 5, 16); + assertTrue(IsoWorkdayUtils.isWeekend(d), () -> "expected weekend, isoDow=" + IsoWorkdayUtils.isoDayOfWeek(d)); + } + + @Test + void may172026_isSunday_weekend() { + assertTrue(IsoWorkdayUtils.isWeekend(LocalDate.of(2026, 5, 17))); + } + + /** 同年月日不同年份:2024-05-16 为周四,勿与 2026-05-16(周六)混淆。 */ + @Test + void may162024_isThursday_notWeekend() { + LocalDate d = LocalDate.of(2024, 5, 16); + assertFalse(IsoWorkdayUtils.isWeekend(d), () -> "expected weekday, isoDow=" + IsoWorkdayUtils.isoDayOfWeek(d)); + } +}