From d872e3e235cbe40322f6b288f9f7a4e57774944e Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Fri, 17 Jul 2026 09:17:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(warehouse):=20=E4=BC=98=E5=8C=96=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=90=86=E5=8A=9F=E8=83=BD=E5=92=8C=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改默认出库类型从销售出库改为生产出库 - 添加产品类别自动推断功能,支持基于历史数据和关键词的智能分类 - 实现库存查询结果显示和选择功能,优化用户体验 - 增强代理计划的消息提示和错误处理机制 - 添加现有产品类别的获取和展示功能 - 优化库存不足和产品未找到的提示信息 - 修复前端表单默认值设置问题 --- .../src/main/resources/application.yml | 1 - .../service/IProductInfoService.java | 1 + .../service/impl/AgentServiceImpl.java | 175 +++++++++++++++++- .../service/impl/ProductInfoServiceImpl.java | 169 +++++++++++++++++ ruoyi-ui/src/views/index.vue | 59 ++++++ ruoyi-ui/src/views/mobile/index.vue | 78 +++++++- .../src/views/warehouse/inRecord/index.vue | 2 +- 7 files changed, 471 insertions(+), 14 deletions(-) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 4d26188..2f3b659 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -12,7 +12,6 @@ ruoyi: addressEnabled: false # 验证码类型 math 数字计算 char 字符验证 captchaType: math - # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IProductInfoService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IProductInfoService.java index 586314c..1030579 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IProductInfoService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IProductInfoService.java @@ -9,6 +9,7 @@ public interface IProductInfoService public List selectProductInfoAll(); public ProductInfo selectProductInfoById(Long id); public boolean checkProductCodeUnique(ProductInfo productInfo); + public String inferProductCategory(ProductInfo productInfo); public int insertProductInfo(ProductInfo productInfo); public int updateProductInfo(ProductInfo productInfo); public int deleteProductInfoByIds(Long[] ids); diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java index b8fbe22..46c49ed 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java @@ -9,8 +9,10 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; @@ -258,8 +260,8 @@ public class AgentServiceImpl implements IAgentService } if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getOutType() == null) { - plan.setOutType(3); - plan.getWarnings().add("未识别出库类型,默认销售出库"); + plan.setOutType(1); + plan.getWarnings().add("未识别出库类型,默认生产出库"); } if (ACTION_CONFIRM_IN.equals(plan.getAction())) @@ -274,7 +276,7 @@ public class AgentServiceImpl implements IAgentService fillStockResults(plan); plan.setNeedConfirm(false); plan.setExecutable(true); - plan.setMessage(plan.getResults().isEmpty() ? "未查询到库存" : "查询到 " + plan.getResults().size() + " 条库存"); + plan.setMessage(plan.getResults().isEmpty() ? stockNotFoundMessage(plan) : "查询到 " + plan.getResults().size() + " 条库存"); if (StringUtils.isBlank(plan.getSummary())) { plan.setSummary("查询库存"); @@ -312,7 +314,7 @@ public class AgentServiceImpl implements IAgentService fillSummary(plan); if (!plan.isExecutable() && StringUtils.isBlank(plan.getMessage())) { - plan.setMessage("信息不完整或存在多个候选,请补充后再执行"); + fillUnableToExecuteMessage(plan); } return plan; } @@ -418,7 +420,8 @@ public class AgentServiceImpl implements IAgentService + "如果用户说第几个、第二个、第2条等,referenceIndex 返回从 1 开始的序号。" + "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。" + "出库类型 outType:1生产出库,2退货出库,3销售出库。" - + "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;新增库区提取 areaName。不要编造信息;无法识别就填 null。"; + + "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;如果用户未提供 category,优先从 existingCategories 选择合适类别,不合适时推断一个简短合理的类别。" + + "除 category 可按规则推断外,不要编造其他字段;无法识别就填 null。新增库区提取 areaName。"; } private String buildUserInput(String text, List contextResults) @@ -426,6 +429,7 @@ public class AgentServiceImpl implements IAgentService JSONObject payload = new JSONObject(); payload.put("text", text); payload.put("contextResults", contextResults == null ? new ArrayList<>() : contextResults); + payload.put("existingCategories", existingCategories()); return payload.toJSONString(); } @@ -850,7 +854,13 @@ public class AgentServiceImpl implements IAgentService ProductInfo product = match.getProduct(); plan.getCandidates().add(new AgentCandidate(product.getId(), "product", product.getProductName(), productDetail(product))); } - plan.setMessage("产品不唯一,请补充名称、规格、品牌或类别"); + if (!ACTION_QUERY_STOCK.equals(plan.getAction())) + { + fillProductChoices(plan, topMatches); + } + plan.setMessage(plan.getResults().isEmpty() + ? "产品不唯一,请选择候选产品,或补充名称、规格、品牌、类别" + : "找到多个匹配产品,请在下方选择要操作的库存项"); } private void resolveReferencedResult(AgentPlan plan, String originalText, List contextResults) @@ -965,6 +975,37 @@ public class AgentServiceImpl implements IAgentService { plan.setUnit(plan.getUnit().trim()); } + if (StringUtils.isBlank(plan.getCategory()) && StringUtils.isNotBlank(plan.getProductName())) + { + ProductInfo draft = new ProductInfo(); + draft.setProductName(plan.getProductName()); + draft.setBrand(plan.getBrand()); + draft.setSpec(plan.getSpec()); + draft.setUnit(plan.getUnit()); + String category = productService.inferProductCategory(draft); + if (StringUtils.isNotBlank(category)) + { + plan.setCategory(category); + plan.getWarnings().add("未提供类别,已自动分类为:" + category); + } + } + } + + private List existingCategories() + { + Set categories = new LinkedHashSet<>(); + for (ProductInfo product : productService.selectProductInfoAll()) + { + if (StringUtils.isNotBlank(product.getCategory())) + { + categories.add(product.getCategory().trim()); + } + if (categories.size() >= 80) + { + break; + } + } + return new ArrayList<>(categories); } private boolean requiresOperator(String action) @@ -987,6 +1028,80 @@ public class AgentServiceImpl implements IAgentService return true; } + private void fillUnableToExecuteMessage(AgentPlan plan) + { + if (plan.getProductId() == null && hasProductClue(plan) && noProductCandidates(plan)) + { + plan.setMessage("没找到产品:" + productClue(plan) + "。请先确认产品名称、品牌、类别或规格是否已维护。"); + return; + } + if (plan.getAreaId() == null && StringUtils.isNotBlank(plan.getAreaName()) && noAreaCandidates(plan)) + { + plan.setMessage("没找到库区:" + plan.getAreaName() + "。请先维护库区后再操作。"); + return; + } + if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getProductId() != null && plan.getResults().isEmpty()) + { + String product = StringUtils.defaultIfBlank(plan.getProductName(), "该产品"); + plan.setMessage("没找到可出库库存:" + product + "。请确认产品是否已有库存,或选择正确库区。"); + return; + } + if (plan.getCandidates() != null && !plan.getCandidates().isEmpty()) + { + plan.setMessage("存在多个候选,请先选择要操作的数据"); + return; + } + if (!validQuantity(plan)) + { + plan.setMessage(ACTION_CREATE_CHECK.equals(plan.getAction()) ? "未识别到实际盘点数量" : "未识别到有效数量"); + return; + } + plan.setMessage("信息不完整,请补充后再执行"); + } + + private boolean noProductCandidates(AgentPlan plan) + { + for (AgentCandidate candidate : plan.getCandidates()) + { + if ("product".equals(candidate.getType())) + { + return false; + } + } + return true; + } + + private boolean hasProductClue(AgentPlan plan) + { + return StringUtils.isNotBlank(plan.getProductName()) + || StringUtils.isNotBlank(plan.getBrand()) + || StringUtils.isNotBlank(plan.getCategory()) + || StringUtils.isNotBlank(plan.getSpec()); + } + + private String productClue(AgentPlan plan) + { + return firstNotBlank(plan.getProductName(), plan.getBrand(), plan.getCategory(), plan.getSpec(), "未识别产品"); + } + + private String stockNotFoundMessage(AgentPlan plan) + { + List parts = new ArrayList<>(); + if (StringUtils.isNotBlank(plan.getAreaName())) + { + parts.add("库区:" + plan.getAreaName()); + } + if (hasProductClue(plan)) + { + parts.add("产品:" + productClue(plan)); + } + if (parts.isEmpty()) + { + return "未查询到库存数据"; + } + return "未查询到符合条件的库存数据(" + String.join(",", parts) + ")"; + } + private void fillActionCandidates(AgentPlan plan) { plan.getCandidates().add(new AgentCandidate(1L, "action", "查库存", "例如:查一下路由器库存")); @@ -1199,6 +1314,51 @@ public class AgentServiceImpl implements IAgentService return item; } + private AgentResultItem toProductResult(ProductInfo product) + { + AgentResultItem item = new AgentResultItem(); + item.setProductId(product.getId()); + item.setProductName(product.getProductName()); + item.setBrand(product.getBrand()); + item.setCategory(product.getCategory()); + item.setSpec(product.getSpec()); + item.setUnit(product.getUnit()); + return item; + } + + private void fillProductChoices(AgentPlan plan, List matches) + { + int count = 0; + for (ScoredProduct match : first(matches, 10)) + { + ProductInfo product = match.getProduct(); + StockInfo query = new StockInfo(); + query.setAreaId(plan.getAreaId()); + query.setProductId(product.getId()); + List stockList = stockInfoService.selectStockInfoList(query); + if (stockList.isEmpty()) + { + if (!ACTION_CREATE_OUT.equals(plan.getAction())) + { + plan.getResults().add(toProductResult(product)); + count++; + } + } + else + { + for (StockInfo stock : stockList) + { + if (count++ >= 20) + { + plan.getWarnings().add("匹配结果较多,仅展示前 20 条"); + return; + } + plan.getResults().add(toResult(stock)); + } + } + } + } + private void fillCurrentStock(AgentPlan plan) { if (plan.getAreaId() == null || plan.getProductId() == null) @@ -1235,7 +1395,8 @@ public class AgentServiceImpl implements IAgentService private void suggestAlternativeStockAreas(AgentPlan plan) { - if (!ACTION_CREATE_OUT.equals(plan.getAction()) || plan.getProductId() == null || plan.isExecutable()) + if (!ACTION_CREATE_OUT.equals(plan.getAction()) || plan.getProductId() == null || plan.isExecutable() + || (plan.getAreaId() == null && (StringUtils.isNotBlank(plan.getAreaName()) || !noAreaCandidates(plan)))) { return; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/ProductInfoServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/ProductInfoServiceImpl.java index da70ffa..74a08a4 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/ProductInfoServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/ProductInfoServiceImpl.java @@ -1,6 +1,9 @@ package com.ruoyi.warehouse.service.impl; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ruoyi.common.constant.UserConstants; @@ -46,6 +49,21 @@ public class ProductInfoServiceImpl implements IProductInfoService return StringUtils.isNull(info) || info.getId().longValue() == id.longValue() ? UserConstants.UNIQUE : UserConstants.NOT_UNIQUE; } + @Override + public String inferProductCategory(ProductInfo productInfo) + { + if (productInfo == null || StringUtils.isBlank(productInfo.getProductName())) + { + return null; + } + String category = inferCategoryFromHistory(productInfo); + if (StringUtils.isNotBlank(category)) + { + return category; + } + return generateCategory(productInfo); + } + @Override public int insertProductInfo(ProductInfo productInfo) { @@ -79,6 +97,10 @@ public class ProductInfoServiceImpl implements IProductInfoService productInfo.setCategory(trimToNull(productInfo.getCategory())); productInfo.setSpec(trimToNull(productInfo.getSpec())); productInfo.setUnit(trimToNull(productInfo.getUnit())); + if (StringUtils.isBlank(productInfo.getCategory()) && StringUtils.isNotBlank(productInfo.getProductName())) + { + productInfo.setCategory(inferProductCategory(productInfo)); + } } private void validateProductInfoUnique(ProductInfo productInfo) @@ -95,4 +117,151 @@ public class ProductInfoServiceImpl implements IProductInfoService { return StringUtils.isBlank(value) ? null : value.trim(); } + + private String inferCategoryFromHistory(ProductInfo productInfo) + { + Map scores = new HashMap<>(); + for (ProductInfo product : productInfoMapper.selectProductInfoAll()) + { + if (StringUtils.isBlank(product.getCategory())) + { + continue; + } + int score = categoryProductScore(productInfo, product); + if (score > 0) + { + String category = product.getCategory().trim(); + scores.put(category, scores.getOrDefault(category, 0) + score); + } + } + String bestCategory = null; + int bestScore = 0; + int secondScore = 0; + for (Map.Entry entry : scores.entrySet()) + { + int score = entry.getValue(); + if (score > bestScore) + { + secondScore = bestScore; + bestScore = score; + bestCategory = entry.getKey(); + } + else if (score > secondScore) + { + secondScore = score; + } + } + if (bestScore >= 4 && bestScore >= secondScore + 2) + { + return bestCategory; + } + return null; + } + + private int categoryProductScore(ProductInfo source, ProductInfo target) + { + int score = 0; + score += fieldScore(source.getProductName(), target.getProductName(), 8); + score += fieldScore(source.getBrand(), target.getBrand(), 6); + score += fieldScore(source.getSpec(), target.getSpec(), 4); + score += fieldScore(source.getUnit(), target.getUnit(), 1); + String draftText = productDraftText(source); + score += containsField(draftText, target.getProductName()) ? 4 : 0; + score += containsField(draftText, target.getBrand()) ? 2 : 0; + score += containsField(draftText, target.getSpec()) ? 2 : 0; + return score; + } + + private int fieldScore(String input, String target, int exactScore) + { + if (StringUtils.isBlank(input) || StringUtils.isBlank(target)) + { + return 0; + } + if (input.trim().equalsIgnoreCase(target.trim())) + { + return exactScore; + } + if (containsIgnoreCase(input, target) || containsIgnoreCase(target, input)) + { + return Math.max(1, exactScore / 2); + } + return 0; + } + + private String productDraftText(ProductInfo productInfo) + { + return StringUtils.defaultString(productInfo.getProductName()) + " " + + StringUtils.defaultString(productInfo.getBrand()) + " " + + StringUtils.defaultString(productInfo.getSpec()) + " " + + StringUtils.defaultString(productInfo.getUnit()); + } + + private String generateCategory(ProductInfo productInfo) + { + String text = productDraftText(productInfo).toLowerCase(Locale.ROOT); + if (containsAny(text, "路由器", "交换机", "网关", "ap", "wifi", "网线", "光模块", "网络")) + { + return "网络设备"; + } + if (containsAny(text, "电脑", "主机", "显示器", "键盘", "鼠标", "硬盘", "内存", "打印机", "扫描仪")) + { + return "办公设备"; + } + if (containsAny(text, "纸", "抽纸", "卷纸", "硒鼓", "墨盒", "墨粉", "笔", "文件夹", "订书", "胶带")) + { + return "办公耗材"; + } + if (containsAny(text, "手套", "口罩", "安全帽", "护目镜", "反光", "劳保")) + { + return "劳保用品"; + } + if (containsAny(text, "扳手", "螺丝刀", "钳", "锤", "钻", "工具")) + { + return "工具"; + } + if (containsAny(text, "螺丝", "螺母", "垫片", "扎带", "卡扣", "五金")) + { + return "五金耗材"; + } + if (containsAny(text, "传感器", "继电器", "电源", "线束", "电缆", "接头", "模块", "芯片", "电阻", "电容")) + { + return "电子元器件"; + } + if (containsAny(text, "机油", "润滑", "清洗剂", "胶水", "胶粘", "油漆", "化学")) + { + return "化工耗材"; + } + if (containsAny(text, "轮胎", "刹车", "电瓶", "电池", "车灯", "滤芯", "汽车")) + { + return "汽车配件"; + } + return "通用物料"; + } + + private boolean containsAny(String text, String... keywords) + { + for (String keyword : keywords) + { + if (containsIgnoreCase(text, keyword)) + { + return true; + } + } + return false; + } + + private boolean containsField(String text, String field) + { + return StringUtils.isNotBlank(text) && StringUtils.isNotBlank(field) && containsIgnoreCase(text, field); + } + + private boolean containsIgnoreCase(String text, String keyword) + { + if (StringUtils.isBlank(text) || StringUtils.isBlank(keyword)) + { + return false; + } + return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT)); + } } diff --git a/ruoyi-ui/src/views/index.vue b/ruoyi-ui/src/views/index.vue index f13d287..de9eef3 100644 --- a/ruoyi-ui/src/views/index.vue +++ b/ruoyi-ui/src/views/index.vue @@ -58,6 +58,11 @@ + + +