diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/AgentController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/AgentController.java new file mode 100644 index 0000000..d7eb058 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/AgentController.java @@ -0,0 +1,32 @@ +package com.ruoyi.web.controller.warehouse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.warehouse.domain.agent.AgentAnalyzeRequest; +import com.ruoyi.warehouse.domain.agent.AgentExecuteRequest; +import com.ruoyi.warehouse.service.IAgentService; + +@RestController +@RequestMapping("/warehouse/agent") +public class AgentController extends BaseController +{ + @Autowired + private IAgentService agentService; + + @PostMapping("/analyze") + public AjaxResult analyze(@RequestBody AgentAnalyzeRequest request) + { + return success(agentService.analyze(request.getText(), request.getContextResults(), getUsername())); + } + + @PostMapping("/execute") + public AjaxResult execute(@RequestBody AgentExecuteRequest request) + { + return success(agentService.execute(request.getPlan(), getUsername())); + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 41929b4..4d26188 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -142,3 +142,10 @@ xss: excludes: /system/notice # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* + +# WMS agent model config. Keep api-key in environment variables, not in source code. +agent: + openai: + base-url: ${OPENAI_BASE_URL:http://192.168.28.10:18080} + api-key: ${OPENAI_API_KEY:sk-dd185946191d42eaa54892dec488bf7b54cad2d7433aee8aac7a017365b0829d} + model: ${OPENAI_MODEL:gpt-5.5} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentAnalyzeRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentAnalyzeRequest.java new file mode 100644 index 0000000..5dfccf8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentAnalyzeRequest.java @@ -0,0 +1,30 @@ +package com.ruoyi.warehouse.domain.agent; + +import java.util.ArrayList; +import java.util.List; + +public class AgentAnalyzeRequest +{ + private String text; + private List contextResults = new ArrayList<>(); + + public String getText() + { + return text; + } + + public void setText(String text) + { + this.text = text; + } + + public List getContextResults() + { + return contextResults; + } + + public void setContextResults(List contextResults) + { + this.contextResults = contextResults; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentCandidate.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentCandidate.java new file mode 100644 index 0000000..533018b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentCandidate.java @@ -0,0 +1,61 @@ +package com.ruoyi.warehouse.domain.agent; + +public class AgentCandidate +{ + private Long id; + private String type; + private String name; + private String detail; + + public AgentCandidate() + { + } + + public AgentCandidate(Long id, String type, String name, String detail) + { + this.id = id; + this.type = type; + this.name = name; + this.detail = detail; + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getDetail() + { + return detail; + } + + public void setDetail(String detail) + { + this.detail = detail; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentExecuteRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentExecuteRequest.java new file mode 100644 index 0000000..d489dd3 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentExecuteRequest.java @@ -0,0 +1,16 @@ +package com.ruoyi.warehouse.domain.agent; + +public class AgentExecuteRequest +{ + private AgentPlan plan; + + public AgentPlan getPlan() + { + return plan; + } + + public void setPlan(AgentPlan plan) + { + this.plan = plan; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentPlan.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentPlan.java new file mode 100644 index 0000000..3a95467 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentPlan.java @@ -0,0 +1,317 @@ +package com.ruoyi.warehouse.domain.agent; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class AgentPlan +{ + private String action; + private String summary; + private String message; + private boolean executable; + private boolean needConfirm; + private Long areaId; + private Long productId; + private Long stockId; + private Long recordId; + private Integer referenceIndex; + private String areaName; + private String productName; + private String brand; + private String category; + private String spec; + private String unit; + private BigDecimal quantity; + private Integer inType; + private Integer outType; + private String operatorName; + private String recordNo; + private String remark; + private BigDecimal currentStock; + private BigDecimal afterStock; + private List warnings = new ArrayList<>(); + private List candidates = new ArrayList<>(); + private List results = new ArrayList<>(); + private AgentPlan nextPlan; + + public String getAction() + { + return action; + } + + public void setAction(String action) + { + this.action = action; + } + + public String getSummary() + { + return summary; + } + + public void setSummary(String summary) + { + this.summary = summary; + } + + public String getMessage() + { + return message; + } + + public void setMessage(String message) + { + this.message = message; + } + + public boolean isExecutable() + { + return executable; + } + + public void setExecutable(boolean executable) + { + this.executable = executable; + } + + public boolean isNeedConfirm() + { + return needConfirm; + } + + public void setNeedConfirm(boolean needConfirm) + { + this.needConfirm = needConfirm; + } + + public Long getAreaId() + { + return areaId; + } + + public void setAreaId(Long areaId) + { + this.areaId = areaId; + } + + public Long getProductId() + { + return productId; + } + + public void setProductId(Long productId) + { + this.productId = productId; + } + + public Long getStockId() + { + return stockId; + } + + public void setStockId(Long stockId) + { + this.stockId = stockId; + } + + public Long getRecordId() + { + return recordId; + } + + public void setRecordId(Long recordId) + { + this.recordId = recordId; + } + + public Integer getReferenceIndex() + { + return referenceIndex; + } + + public void setReferenceIndex(Integer referenceIndex) + { + this.referenceIndex = referenceIndex; + } + + public String getAreaName() + { + return areaName; + } + + public void setAreaName(String areaName) + { + this.areaName = areaName; + } + + public String getProductName() + { + return productName; + } + + public void setProductName(String productName) + { + this.productName = productName; + } + + public String getBrand() + { + return brand; + } + + public void setBrand(String brand) + { + this.brand = brand; + } + + public String getCategory() + { + return category; + } + + public void setCategory(String category) + { + this.category = category; + } + + public String getSpec() + { + return spec; + } + + public void setSpec(String spec) + { + this.spec = spec; + } + + public String getUnit() + { + return unit; + } + + public void setUnit(String unit) + { + this.unit = unit; + } + + public BigDecimal getQuantity() + { + return quantity; + } + + public void setQuantity(BigDecimal quantity) + { + this.quantity = quantity; + } + + public Integer getInType() + { + return inType; + } + + public void setInType(Integer inType) + { + this.inType = inType; + } + + public Integer getOutType() + { + return outType; + } + + public void setOutType(Integer outType) + { + this.outType = outType; + } + + public String getOperatorName() + { + return operatorName; + } + + public void setOperatorName(String operatorName) + { + this.operatorName = operatorName; + } + + public String getRecordNo() + { + return recordNo; + } + + public void setRecordNo(String recordNo) + { + this.recordNo = recordNo; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public BigDecimal getCurrentStock() + { + return currentStock; + } + + public void setCurrentStock(BigDecimal currentStock) + { + this.currentStock = currentStock; + } + + public BigDecimal getAfterStock() + { + return afterStock; + } + + public void setAfterStock(BigDecimal afterStock) + { + this.afterStock = afterStock; + } + + public List getWarnings() + { + return warnings; + } + + public void setWarnings(List warnings) + { + this.warnings = warnings; + } + + public List getCandidates() + { + return candidates; + } + + public void setCandidates(List candidates) + { + this.candidates = candidates; + } + + public List getResults() + { + return results; + } + + public void setResults(List results) + { + this.results = results; + } + + public AgentPlan getNextPlan() + { + return nextPlan; + } + + public void setNextPlan(AgentPlan nextPlan) + { + this.nextPlan = nextPlan; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentResultItem.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentResultItem.java new file mode 100644 index 0000000..d5c5dc1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/agent/AgentResultItem.java @@ -0,0 +1,128 @@ +package com.ruoyi.warehouse.domain.agent; + +import java.math.BigDecimal; + +public class AgentResultItem +{ + private Long id; + private Long areaId; + private Long productId; + private String areaName; + private String productName; + private String brand; + private String category; + private String spec; + private String unit; + private BigDecimal stockNum; + private String recordNo; + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public Long getAreaId() + { + return areaId; + } + + public void setAreaId(Long areaId) + { + this.areaId = areaId; + } + + public Long getProductId() + { + return productId; + } + + public void setProductId(Long productId) + { + this.productId = productId; + } + + public String getAreaName() + { + return areaName; + } + + public void setAreaName(String areaName) + { + this.areaName = areaName; + } + + public String getProductName() + { + return productName; + } + + public void setProductName(String productName) + { + this.productName = productName; + } + + public String getBrand() + { + return brand; + } + + public void setBrand(String brand) + { + this.brand = brand; + } + + public String getCategory() + { + return category; + } + + public void setCategory(String category) + { + this.category = category; + } + + public String getSpec() + { + return spec; + } + + public void setSpec(String spec) + { + this.spec = spec; + } + + public String getUnit() + { + return unit; + } + + public void setUnit(String unit) + { + this.unit = unit; + } + + public BigDecimal getStockNum() + { + return stockNum; + } + + public void setStockNum(BigDecimal stockNum) + { + this.stockNum = stockNum; + } + + public String getRecordNo() + { + return recordNo; + } + + public void setRecordNo(String recordNo) + { + this.recordNo = recordNo; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IAgentService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IAgentService.java new file mode 100644 index 0000000..8cbcf68 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IAgentService.java @@ -0,0 +1,12 @@ +package com.ruoyi.warehouse.service; + +import java.util.List; +import com.ruoyi.warehouse.domain.agent.AgentPlan; +import com.ruoyi.warehouse.domain.agent.AgentResultItem; + +public interface IAgentService +{ + public AgentPlan analyze(String text, List contextResults, String username); + + public AgentPlan execute(AgentPlan plan, String username); +} 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 new file mode 100644 index 0000000..b8fbe22 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java @@ -0,0 +1,1393 @@ +package com.ruoyi.warehouse.service.impl; + +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; +import com.ruoyi.warehouse.domain.InRecord; +import com.ruoyi.warehouse.domain.OutRecord; +import com.ruoyi.warehouse.domain.ProductInfo; +import com.ruoyi.warehouse.domain.StockCheckRecord; +import com.ruoyi.warehouse.domain.StockInfo; +import com.ruoyi.warehouse.domain.WarehouseArea; +import com.ruoyi.warehouse.domain.agent.AgentCandidate; +import com.ruoyi.warehouse.domain.agent.AgentPlan; +import com.ruoyi.warehouse.domain.agent.AgentResultItem; +import com.ruoyi.warehouse.service.IAgentService; +import com.ruoyi.warehouse.service.IInRecordService; +import com.ruoyi.warehouse.service.IOutRecordService; +import com.ruoyi.warehouse.service.IProductInfoService; +import com.ruoyi.warehouse.service.IStockCheckRecordService; +import com.ruoyi.warehouse.service.IStockInfoService; +import com.ruoyi.warehouse.service.IWarehouseAreaService; + +@Service +public class AgentServiceImpl implements IAgentService +{ + private static final String ACTION_QUERY_STOCK = "query_stock"; + private static final String ACTION_CREATE_IN = "create_in_record"; + private static final String ACTION_CONFIRM_IN = "confirm_in_record"; + private static final String ACTION_CREATE_OUT = "create_out_record"; + private static final String ACTION_CREATE_CHECK = "create_stock_check"; + private static final String ACTION_CREATE_PRODUCT = "create_product"; + private static final String ACTION_CREATE_AREA = "create_area"; + private static final String ACTION_UNKNOWN = "unknown"; + + private static final Pattern NUMBER_PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)"); + private static final Pattern IN_NO_PATTERN = Pattern.compile("(IN\\d+)", Pattern.CASE_INSENSITIVE); + private static final Pattern INDEX_PATTERN = Pattern.compile("第\\s*([0-9一二两三四五六七八九十]+)\\s*(个|条|项)?"); + + @Value("${agent.openai.base-url}") + private String baseUrl; + + @Value("${agent.openai.api-key}") + private String apiKey; + + @Value("${agent.openai.model}") + private String model; + + @Autowired + private IWarehouseAreaService areaService; + + @Autowired + private IProductInfoService productService; + + @Autowired + private IStockInfoService stockInfoService; + + @Autowired + private IInRecordService inRecordService; + + @Autowired + private IOutRecordService outRecordService; + + @Autowired + private IStockCheckRecordService stockCheckRecordService; + + @Autowired + private ISysUserService userService; + + @Override + public AgentPlan analyze(String text, List contextResults, String username) + { + if (StringUtils.isBlank(text)) + { + throw new ServiceException("请输入要操作的内容"); + } + + AgentPlan plan = null; + List warnings = new ArrayList<>(); + if (StringUtils.isBlank(apiKey)) + { + warnings.add("未配置 OPENAI_API_KEY,已使用基础规则解析"); + } + else + { + try + { + plan = callModel(text, contextResults); + } + catch (Exception e) + { + warnings.add("大模型解析失败,已使用基础规则解析:" + e.getMessage()); + } + } + if (plan == null) + { + plan = fallbackParse(text); + } + plan.getWarnings().addAll(warnings); + return normalizePlan(plan, text, username, contextResults); + } + + @Override + @Transactional + public AgentPlan execute(AgentPlan plan, String username) + { + if (plan == null || StringUtils.isBlank(plan.getAction())) + { + throw new ServiceException("缺少可执行的操作草稿"); + } + AgentPlan checkedPlan = normalizePlan(plan, null, username, null); + if (!checkedPlan.isExecutable()) + { + throw new ServiceException(StringUtils.defaultIfBlank(checkedPlan.getMessage(), "当前草稿不能执行")); + } + + if (ACTION_QUERY_STOCK.equals(checkedPlan.getAction())) + { + checkedPlan.setMessage("查询完成"); + checkedPlan.setNeedConfirm(false); + return checkedPlan; + } + if (ACTION_CREATE_IN.equals(checkedPlan.getAction())) + { + InRecord record = new InRecord(); + record.setAreaId(checkedPlan.getAreaId()); + record.setProductId(checkedPlan.getProductId()); + record.setInType(checkedPlan.getInType()); + record.setInNum(checkedPlan.getQuantity()); + record.setInOperator(checkedPlan.getOperatorName()); + record.setRemark(checkedPlan.getRemark()); + record.setOperator(username); + inRecordService.insertInRecord(record); + checkedPlan.setRecordId(record.getId()); + checkedPlan.setRecordNo(record.getInNo()); + checkedPlan.setMessage("已生成待入库记录:" + record.getInNo()); + checkedPlan.setNeedConfirm(false); + return checkedPlan; + } + if (ACTION_CONFIRM_IN.equals(checkedPlan.getAction())) + { + inRecordService.confirmInRecord(checkedPlan.getRecordId(), username); + checkedPlan.setMessage("已确认入库:" + checkedPlan.getRecordNo()); + checkedPlan.setNeedConfirm(false); + return checkedPlan; + } + if (ACTION_CREATE_OUT.equals(checkedPlan.getAction())) + { + OutRecord record = new OutRecord(); + record.setAreaId(checkedPlan.getAreaId()); + record.setProductId(checkedPlan.getProductId()); + record.setOutType(checkedPlan.getOutType()); + record.setOutNum(checkedPlan.getQuantity()); + record.setOutOperator(checkedPlan.getOperatorName()); + record.setRemark(checkedPlan.getRemark()); + record.setOperator(username); + outRecordService.insertOutRecord(record); + checkedPlan.setRecordId(record.getId()); + checkedPlan.setRecordNo(record.getOutNo()); + checkedPlan.setMessage("已完成出库并生成记录:" + record.getOutNo()); + checkedPlan.setNeedConfirm(false); + return checkedPlan; + } + if (ACTION_CREATE_CHECK.equals(checkedPlan.getAction())) + { + StockCheckRecord record = new StockCheckRecord(); + record.setAreaId(checkedPlan.getAreaId()); + record.setProductId(checkedPlan.getProductId()); + record.setRealNum(checkedPlan.getQuantity()); + record.setCheckUser(checkedPlan.getOperatorName()); + record.setReason(checkedPlan.getRemark()); + stockCheckRecordService.insertStockCheckRecord(record); + checkedPlan.setRecordId(record.getId()); + checkedPlan.setRecordNo(record.getCheckNo()); + checkedPlan.setMessage("已生成待确认盘库记录:" + record.getCheckNo()); + checkedPlan.setNeedConfirm(false); + return checkedPlan; + } + if (ACTION_CREATE_PRODUCT.equals(checkedPlan.getAction())) + { + ProductInfo product = new ProductInfo(); + product.setProductName(checkedPlan.getProductName()); + product.setBrand(checkedPlan.getBrand()); + product.setCategory(checkedPlan.getCategory()); + product.setSpec(checkedPlan.getSpec()); + product.setUnit(checkedPlan.getUnit()); + product.setRemark(checkedPlan.getRemark()); + productService.insertProductInfo(product); + checkedPlan.setProductId(product.getId()); + checkedPlan.setMessage("已添加产品:" + product.getProductName()); + checkedPlan.setNeedConfirm(false); + if (checkedPlan.getNextPlan() != null) + { + checkedPlan.getNextPlan().setProductId(product.getId()); + fillProduct(checkedPlan.getNextPlan(), product); + normalizePlan(checkedPlan.getNextPlan(), null, username, null); + checkedPlan.setMessage("已添加产品,并生成下一步草稿"); + } + return checkedPlan; + } + if (ACTION_CREATE_AREA.equals(checkedPlan.getAction())) + { + WarehouseArea area = new WarehouseArea(); + area.setAreaName(checkedPlan.getAreaName()); + area.setRemark(checkedPlan.getRemark()); + areaService.insertWarehouseArea(area); + checkedPlan.setAreaId(area.getId()); + checkedPlan.setMessage("已添加库区:" + area.getAreaName()); + checkedPlan.setNeedConfirm(false); + if (checkedPlan.getNextPlan() != null) + { + checkedPlan.getNextPlan().setAreaId(area.getId()); + checkedPlan.getNextPlan().setAreaName(area.getAreaName()); + normalizePlan(checkedPlan.getNextPlan(), null, username, null); + checkedPlan.setMessage("已添加库区,并生成下一步草稿"); + } + return checkedPlan; + } + throw new ServiceException("暂不支持该操作"); + } + + private AgentPlan normalizePlan(AgentPlan plan, String originalText, String username, List contextResults) + { + if (StringUtils.isBlank(plan.getAction())) + { + plan.setAction(ACTION_UNKNOWN); + } + if (requiresOperator(plan.getAction()) && StringUtils.isBlank(plan.getOperatorName())) + { + plan.setOperatorName(resolveNickName(username)); + plan.getWarnings().add("未识别到人员,默认使用当前登录用户"); + } + + if (ACTION_CREATE_IN.equals(plan.getAction()) && plan.getInType() == null) + { + plan.setInType(2); + plan.getWarnings().add("未识别入库类型,默认采购入库"); + } + if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getOutType() == null) + { + plan.setOutType(3); + plan.getWarnings().add("未识别出库类型,默认销售出库"); + } + + if (ACTION_CONFIRM_IN.equals(plan.getAction())) + { + resolveInRecord(plan, originalText); + return plan; + } + if (ACTION_QUERY_STOCK.equals(plan.getAction())) + { + resolveArea(plan, originalText, false); + resolveProduct(plan, originalText, false); + fillStockResults(plan); + plan.setNeedConfirm(false); + plan.setExecutable(true); + plan.setMessage(plan.getResults().isEmpty() ? "未查询到库存" : "查询到 " + plan.getResults().size() + " 条库存"); + if (StringUtils.isBlank(plan.getSummary())) + { + plan.setSummary("查询库存"); + } + return plan; + } + + if (ACTION_CREATE_IN.equals(plan.getAction()) || ACTION_CREATE_OUT.equals(plan.getAction()) || ACTION_CREATE_CHECK.equals(plan.getAction())) + { + resolveReferencedResult(plan, originalText, contextResults); + resolveArea(plan, originalText, true); + resolveProduct(plan, originalText, true); + if ((ACTION_CREATE_IN.equals(plan.getAction()) || ACTION_CREATE_CHECK.equals(plan.getAction())) + && plan.getAreaId() == null && StringUtils.isNotBlank(plan.getAreaName()) && noAreaCandidates(plan)) + { + AgentPlan areaPlan = buildAreaPlanFrom(plan); + areaPlan.setNextPlan(copyOperationPlan(plan)); + normalizePlan(areaPlan, originalText, username, contextResults); + areaPlan.setMessage("库区不存在,请先确认添加库区,添加后会继续生成操作草稿"); + return areaPlan; + } + if (ACTION_CREATE_IN.equals(plan.getAction()) && plan.getProductId() == null && plan.getCandidates().isEmpty()) + { + AgentPlan productPlan = buildProductPlanFrom(plan); + productPlan.setNextPlan(copyInPlan(plan)); + normalizePlan(productPlan, originalText, username, contextResults); + productPlan.setMessage("产品信息不存在,请先确认添加产品,添加后会继续生成入库草稿"); + return productPlan; + } + validateQuantity(plan); + plan.setNeedConfirm(true); + plan.setExecutable(plan.getAreaId() != null && plan.getProductId() != null && validQuantity(plan) && plan.getCandidates().isEmpty()); + fillCurrentStock(plan); + suggestAlternativeStockAreas(plan); + fillSummary(plan); + if (!plan.isExecutable() && StringUtils.isBlank(plan.getMessage())) + { + plan.setMessage("信息不完整或存在多个候选,请补充后再执行"); + } + return plan; + } + + if (ACTION_CREATE_PRODUCT.equals(plan.getAction())) + { + normalizeProductPlan(plan); + ProductInfo sameProduct = findSameProduct(plan); + if (sameProduct != null) + { + plan.setProductId(sameProduct.getId()); + plan.setExecutable(false); + plan.setNeedConfirm(false); + plan.setMessage("该产品已存在:" + sameProduct.getProductName()); + return plan; + } + plan.setNeedConfirm(true); + plan.setExecutable(StringUtils.isNotBlank(plan.getProductName())); + if (StringUtils.isBlank(plan.getMessage())) + { + plan.setMessage(plan.isExecutable() ? "请确认是否添加该产品" : "请补充产品名称"); + } + fillSummary(plan); + return plan; + } + + if (ACTION_CREATE_AREA.equals(plan.getAction())) + { + WarehouseArea sameArea = findSameArea(plan.getAreaName()); + if (sameArea != null) + { + plan.setAreaId(sameArea.getId()); + plan.setExecutable(false); + plan.setNeedConfirm(false); + plan.setMessage("该库区已存在:" + sameArea.getAreaName()); + return plan; + } + plan.setNeedConfirm(true); + plan.setExecutable(StringUtils.isNotBlank(plan.getAreaName())); + if (StringUtils.isBlank(plan.getMessage())) + { + plan.setMessage(plan.isExecutable() ? "请确认是否添加该库区" : "请补充库区名称"); + } + fillSummary(plan); + return plan; + } + + plan.setExecutable(false); + plan.setNeedConfirm(false); + fillActionCandidates(plan); + plan.setMessage(plan.getCandidates().isEmpty() ? "暂时只能处理库存查询、入库登记、确认入库、出库、盘库、新增产品和新增库区" : "我不确定你要做什么,请选择操作类型后再说一次"); + return plan; + } + + private AgentPlan callModel(String text, List contextResults) throws Exception + { + JSONObject body = new JSONObject(); + body.put("model", model); + JSONArray input = new JSONArray(); + input.add(message("system", systemPrompt())); + input.add(message("user", buildUserInput(text, contextResults))); + body.put("input", input); + JSONObject format = new JSONObject(); + format.put("type", "json_object"); + JSONObject textFormat = new JSONObject(); + textFormat.put("format", format); + body.put("text", textFormat); + + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(normalizeEndpoint(baseUrl))) + .timeout(Duration.ofSeconds(30)) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) + { + throw new ServiceException("HTTP " + response.statusCode()); + } + String output = extractOutputText(response.body()); + if (StringUtils.isBlank(output)) + { + throw new ServiceException("模型未返回内容"); + } + return JSON.parseObject(extractJson(output), AgentPlan.class); + } + + private JSONObject message(String role, String content) + { + JSONObject message = new JSONObject(); + message.put("role", role); + message.put("content", content); + return message; + } + + private String systemPrompt() + { + return "你是库存管理系统的意图解析器,只返回一个 JSON 对象,不要解释。" + + "action 只能是 query_stock, create_in_record, confirm_in_record, create_out_record, create_stock_check, create_product, create_area, unknown。" + + "字段包括 action, areaName, productName, brand, category, spec, unit, quantity, inType, outType, operatorName, recordNo, remark, summary, referenceIndex。" + + "如果用户说第几个、第二个、第2条等,referenceIndex 返回从 1 开始的序号。" + + "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。" + + "出库类型 outType:1生产出库,2退货出库,3销售出库。" + + "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;新增库区提取 areaName。不要编造信息;无法识别就填 null。"; + } + + private String buildUserInput(String text, List contextResults) + { + JSONObject payload = new JSONObject(); + payload.put("text", text); + payload.put("contextResults", contextResults == null ? new ArrayList<>() : contextResults); + return payload.toJSONString(); + } + + private String normalizeEndpoint(String configuredBaseUrl) + { + String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim(); + while (url.endsWith("/")) + { + url = url.substring(0, url.length() - 1); + } + if (url.endsWith("/v1")) + { + return url + "/responses"; + } + if (url.endsWith("/v1/responses")) + { + return url; + } + return url + "/v1/responses"; + } + + private String extractOutputText(String responseBody) + { + JSONObject root = JSON.parseObject(responseBody); + String outputText = root.getString("output_text"); + if (StringUtils.isNotBlank(outputText)) + { + return outputText; + } + StringBuilder builder = new StringBuilder(); + JSONArray output = root.getJSONArray("output"); + if (output != null) + { + for (int i = 0; i < output.size(); i++) + { + JSONObject item = output.getJSONObject(i); + JSONArray content = item == null ? null : item.getJSONArray("content"); + if (content == null) + { + continue; + } + for (int j = 0; j < content.size(); j++) + { + JSONObject contentItem = content.getJSONObject(j); + if (contentItem != null && StringUtils.isNotBlank(contentItem.getString("text"))) + { + builder.append(contentItem.getString("text")); + } + } + } + } + return builder.toString(); + } + + private String extractJson(String output) + { + String text = output.trim(); + int start = text.indexOf('{'); + int end = text.lastIndexOf('}'); + if (start >= 0 && end > start) + { + return text.substring(start, end + 1); + } + return text; + } + + private AgentPlan fallbackParse(String text) + { + String source = StringUtils.defaultString(text); + AgentPlan plan = new AgentPlan(); + if ((source.contains("添加") || source.contains("新增") || source.contains("录入")) && source.contains("库区")) + { + plan.setAction(ACTION_CREATE_AREA); + plan.setAreaName(extractAreaName(source)); + } + else if ((source.contains("添加") || source.contains("新增") || source.contains("录入")) && source.contains("产品")) + { + plan.setAction(ACTION_CREATE_PRODUCT); + fillProductDraft(plan, source); + } + else if (source.contains("确认") && source.contains("入库")) + { + plan.setAction(ACTION_CONFIRM_IN); + } + else if (source.contains("出库")) + { + plan.setAction(ACTION_CREATE_OUT); + fillProductDraft(plan, source); + } + else if (source.contains("盘")) + { + plan.setAction(ACTION_CREATE_CHECK); + fillProductDraft(plan, source); + } + else if (source.contains("入库")) + { + plan.setAction(ACTION_CREATE_IN); + fillProductDraft(plan, source); + } + else if (source.contains("库存") || source.contains("查询") || source.contains("查")) + { + plan.setAction(ACTION_QUERY_STOCK); + } + else + { + plan.setAction(ACTION_UNKNOWN); + } + plan.setQuantity(extractQuantity(source)); + plan.setReferenceIndex(extractReferenceIndex(source)); + if (ACTION_QUERY_STOCK.equals(plan.getAction())) + { + plan.setProductName(extractStockKeyword(source)); + } + plan.setRecordNo(extractInNo(source)); + plan.setInType(resolveInType(source)); + plan.setOutType(resolveOutType(source)); + plan.setRemark(source); + return plan; + } + + private Integer extractReferenceIndex(String text) + { + Matcher matcher = INDEX_PATTERN.matcher(StringUtils.defaultString(text)); + if (matcher.find()) + { + return parseIndex(matcher.group(1)); + } + return null; + } + + private Integer parseIndex(String value) + { + if (StringUtils.isBlank(value)) + { + return null; + } + String text = value.trim(); + if (text.matches("\\d+")) + { + return Integer.valueOf(text); + } + if ("一".equals(text)) + { + return 1; + } + if ("二".equals(text) || "两".equals(text)) + { + return 2; + } + if ("三".equals(text)) + { + return 3; + } + if ("四".equals(text)) + { + return 4; + } + if ("五".equals(text)) + { + return 5; + } + if ("六".equals(text)) + { + return 6; + } + if ("七".equals(text)) + { + return 7; + } + if ("八".equals(text)) + { + return 8; + } + if ("九".equals(text)) + { + return 9; + } + if ("十".equals(text)) + { + return 10; + } + if (text.startsWith("十")) + { + Integer tail = parseIndex(text.substring(1)); + return tail == null ? 10 : 10 + tail; + } + if (text.endsWith("十")) + { + Integer head = parseIndex(text.substring(0, text.length() - 1)); + return head == null ? null : head * 10; + } + int tenIndex = text.indexOf("十"); + if (tenIndex > 0) + { + Integer head = parseIndex(text.substring(0, tenIndex)); + Integer tail = parseIndex(text.substring(tenIndex + 1)); + return head == null ? null : head * 10 + (tail == null ? 0 : tail); + } + return null; + } + + private String extractStockKeyword(String text) + { + String keyword = StringUtils.defaultString(text); + keyword = keyword.replaceAll("帮我|请|一下|查一下|查询|查|库存|有多少|多少|还有|的", " "); + keyword = keyword.replaceAll("\\s+", " ").trim(); + return StringUtils.isBlank(keyword) ? null : keyword; + } + + private void fillProductDraft(AgentPlan plan, String text) + { + String content = StringUtils.defaultString(text) + .replaceAll("帮我|请|添加|新增|录入|一个|一条|产品|商品|物料|入库|出库|盘库|盘点|采购|生产|销售|退货|归还", " ") + .replaceAll("[,,;;、]", ",") + .replaceAll("\\s+", " ") + .trim(); + if (StringUtils.isBlank(content)) + { + return; + } + String[] parts = content.split(","); + List values = new ArrayList<>(); + for (String part : parts) + { + String value = part.trim(); + if (StringUtils.isNotBlank(value) && !value.matches("[0-9]+(?:\\.[0-9]+)?(个|件|箱|支|台|套|包)?")) + { + values.add(value); + } + } + if (!values.isEmpty() && StringUtils.isBlank(plan.getProductName())) + { + plan.setProductName(values.get(0)); + } + if (values.size() > 1 && StringUtils.isBlank(plan.getBrand())) + { + plan.setBrand(values.get(1)); + } + if (values.size() > 2 && StringUtils.isBlank(plan.getSpec())) + { + plan.setSpec(values.get(2)); + } + if (values.size() > 3 && StringUtils.isBlank(plan.getCategory())) + { + plan.setCategory(values.get(3)); + } + if (StringUtils.isBlank(plan.getUnit())) + { + plan.setUnit(extractUnit(text)); + } + } + + private String extractAreaName(String text) + { + String areaName = StringUtils.defaultString(text) + .replaceAll("帮我|请|添加|新增|录入|一个|一条|库区|仓库|区域", " ") + .replaceAll("[,,;;、]", " ") + .replaceAll("\\s+", " ") + .trim(); + return StringUtils.isBlank(areaName) ? null : areaName; + } + + private String extractUnit(String text) + { + Matcher matcher = Pattern.compile("[0-9]+(?:\\.[0-9]+)?\\s*(个|件|箱|支|台|套|包|卷|瓶|袋|张|本)").matcher(StringUtils.defaultString(text)); + if (matcher.find()) + { + return matcher.group(1); + } + return null; + } + + private BigDecimal extractQuantity(String text) + { + Matcher matcher = NUMBER_PATTERN.matcher(StringUtils.defaultString(text)); + BigDecimal quantity = null; + while (matcher.find()) + { + if (matcher.start() > 0 && text.charAt(matcher.start() - 1) == '第') + { + continue; + } + quantity = new BigDecimal(matcher.group(1)); + } + return quantity; + } + + private String extractInNo(String text) + { + Matcher matcher = IN_NO_PATTERN.matcher(StringUtils.defaultString(text)); + if (matcher.find()) + { + return matcher.group(1).toUpperCase(Locale.ROOT); + } + return null; + } + + private Integer resolveInType(String text) + { + if (StringUtils.isBlank(text)) + { + return null; + } + if (text.contains("生产")) + { + return 1; + } + if (text.contains("归还")) + { + return 3; + } + if (text.contains("退货")) + { + return 4; + } + return text.contains("入库") ? 2 : null; + } + + private Integer resolveOutType(String text) + { + if (StringUtils.isBlank(text)) + { + return null; + } + if (text.contains("生产")) + { + return 1; + } + if (text.contains("退货")) + { + return 2; + } + return text.contains("出库") ? 3 : null; + } + + private void resolveArea(AgentPlan plan, String originalText, boolean required) + { + if (plan.getAreaId() != null) + { + WarehouseArea area = areaService.selectWarehouseAreaById(plan.getAreaId()); + if (area != null) + { + plan.setAreaName(area.getAreaName()); + return; + } + } + List areas = areaService.selectWarehouseAreaAll(); + List matches = new ArrayList<>(); + for (WarehouseArea area : areas) + { + if (matchesText(plan.getAreaName(), area.getAreaName(), area.getAreaCode()) + || matchesText(originalText, area.getAreaName(), area.getAreaCode())) + { + matches.add(area); + } + } + if (matches.size() == 1) + { + WarehouseArea area = matches.get(0); + plan.setAreaId(area.getId()); + plan.setAreaName(area.getAreaName()); + } + else if (matches.size() > 1) + { + for (WarehouseArea area : first(matches, 5)) + { + plan.getCandidates().add(new AgentCandidate(area.getId(), "area", area.getAreaName(), area.getAreaCode())); + } + plan.setMessage("库区不唯一,请补充更明确的库区"); + } + else if (required) + { + plan.getWarnings().add("未识别到库区"); + } + } + + private void resolveProduct(AgentPlan plan, String originalText, boolean required) + { + if (plan.getProductId() != null) + { + ProductInfo product = productService.selectProductInfoById(plan.getProductId()); + if (product != null) + { + fillProduct(plan, product); + return; + } + } + List matches = new ArrayList<>(); + for (ProductInfo product : productService.selectProductInfoAll()) + { + int score = productScore(plan, originalText, product); + if (score > 0) + { + matches.add(new ScoredProduct(product, score)); + } + } + matches.sort(Comparator.comparingInt(ScoredProduct::getScore).reversed()); + if (matches.isEmpty()) + { + if (required) + { + plan.getWarnings().add("未识别到产品"); + } + return; + } + int topScore = matches.get(0).getScore(); + List topMatches = new ArrayList<>(); + for (ScoredProduct match : matches) + { + if (match.getScore() == topScore) + { + topMatches.add(match); + } + } + if (topMatches.size() == 1) + { + fillProduct(plan, topMatches.get(0).getProduct()); + return; + } + for (ScoredProduct match : first(topMatches, 5)) + { + ProductInfo product = match.getProduct(); + plan.getCandidates().add(new AgentCandidate(product.getId(), "product", product.getProductName(), productDetail(product))); + } + plan.setMessage("产品不唯一,请补充名称、规格、品牌或类别"); + } + + private void resolveReferencedResult(AgentPlan plan, String originalText, List contextResults) + { + if (plan.getReferenceIndex() == null) + { + plan.setReferenceIndex(extractReferenceIndex(originalText)); + } + if (plan.getReferenceIndex() == null) + { + return; + } + if (contextResults == null || contextResults.isEmpty()) + { + plan.setMessage("没有可引用的上次查询结果,请先查询库存"); + return; + } + int index = plan.getReferenceIndex() - 1; + if (index < 0 || index >= contextResults.size()) + { + plan.setMessage("上次查询结果没有第 " + plan.getReferenceIndex() + " 项"); + return; + } + AgentResultItem item = contextResults.get(index); + plan.setStockId(item.getId()); + plan.setAreaId(item.getAreaId()); + plan.setProductId(item.getProductId()); + plan.setAreaName(item.getAreaName()); + plan.setProductName(item.getProductName()); + plan.setBrand(item.getBrand()); + plan.setCategory(item.getCategory()); + plan.setSpec(item.getSpec()); + plan.setUnit(item.getUnit()); + plan.setCurrentStock(item.getStockNum()); + plan.getWarnings().add("已引用上次查询的第 " + plan.getReferenceIndex() + " 项"); + } + + private AgentPlan buildProductPlanFrom(AgentPlan source) + { + AgentPlan plan = new AgentPlan(); + plan.setAction(ACTION_CREATE_PRODUCT); + plan.setProductName(source.getProductName()); + plan.setBrand(source.getBrand()); + plan.setCategory(source.getCategory()); + plan.setSpec(source.getSpec()); + plan.setUnit(source.getUnit()); + plan.setRemark(source.getRemark()); + return plan; + } + + private AgentPlan buildAreaPlanFrom(AgentPlan source) + { + AgentPlan plan = new AgentPlan(); + plan.setAction(ACTION_CREATE_AREA); + plan.setAreaName(source.getAreaName()); + plan.setRemark(source.getRemark()); + return plan; + } + + private AgentPlan copyInPlan(AgentPlan source) + { + AgentPlan plan = new AgentPlan(); + plan.setAction(ACTION_CREATE_IN); + plan.setAreaId(source.getAreaId()); + plan.setAreaName(source.getAreaName()); + plan.setQuantity(source.getQuantity()); + plan.setInType(source.getInType()); + plan.setOperatorName(source.getOperatorName()); + plan.setRemark(source.getRemark()); + return plan; + } + + private AgentPlan copyOperationPlan(AgentPlan source) + { + AgentPlan plan = new AgentPlan(); + plan.setAction(source.getAction()); + plan.setAreaId(source.getAreaId()); + plan.setProductId(source.getProductId()); + plan.setAreaName(source.getAreaName()); + plan.setProductName(source.getProductName()); + plan.setBrand(source.getBrand()); + plan.setCategory(source.getCategory()); + plan.setSpec(source.getSpec()); + plan.setUnit(source.getUnit()); + plan.setQuantity(source.getQuantity()); + plan.setInType(source.getInType()); + plan.setOutType(source.getOutType()); + plan.setOperatorName(source.getOperatorName()); + plan.setRemark(source.getRemark()); + return plan; + } + + private void normalizeProductPlan(AgentPlan plan) + { + if (StringUtils.isNotBlank(plan.getProductName())) + { + plan.setProductName(plan.getProductName().trim()); + } + if (StringUtils.isNotBlank(plan.getBrand())) + { + plan.setBrand(plan.getBrand().trim()); + } + if (StringUtils.isNotBlank(plan.getCategory())) + { + plan.setCategory(plan.getCategory().trim()); + } + if (StringUtils.isNotBlank(plan.getSpec())) + { + plan.setSpec(plan.getSpec().trim()); + } + if (StringUtils.isNotBlank(plan.getUnit())) + { + plan.setUnit(plan.getUnit().trim()); + } + } + + private boolean requiresOperator(String action) + { + return ACTION_CREATE_IN.equals(action) + || ACTION_CONFIRM_IN.equals(action) + || ACTION_CREATE_OUT.equals(action) + || ACTION_CREATE_CHECK.equals(action); + } + + private boolean noAreaCandidates(AgentPlan plan) + { + for (AgentCandidate candidate : plan.getCandidates()) + { + if ("area".equals(candidate.getType())) + { + return false; + } + } + return true; + } + + private void fillActionCandidates(AgentPlan plan) + { + plan.getCandidates().add(new AgentCandidate(1L, "action", "查库存", "例如:查一下路由器库存")); + plan.getCandidates().add(new AgentCandidate(2L, "action", "入库登记", "例如:采购入库路由器10个到一号库")); + plan.getCandidates().add(new AgentCandidate(3L, "action", "出库", "例如:第二个出库1个")); + plan.getCandidates().add(new AgentCandidate(4L, "action", "盘库", "例如:盘点路由器实际5个")); + plan.getCandidates().add(new AgentCandidate(5L, "action", "新增产品", "例如:添加产品纸,心心相印,3层120抽")); + plan.getCandidates().add(new AgentCandidate(6L, "action", "新增库区", "例如:新增库区一号库")); + } + + private ProductInfo findSameProduct(AgentPlan plan) + { + for (ProductInfo product : productService.selectProductInfoAll()) + { + if (sameText(product.getProductName(), plan.getProductName()) + && sameText(product.getBrand(), plan.getBrand()) + && sameText(product.getCategory(), plan.getCategory()) + && sameText(product.getSpec(), plan.getSpec())) + { + return product; + } + } + return null; + } + + private WarehouseArea findSameArea(String areaName) + { + if (StringUtils.isBlank(areaName)) + { + return null; + } + for (WarehouseArea area : areaService.selectWarehouseAreaAll()) + { + if (sameText(area.getAreaName(), areaName)) + { + return area; + } + } + return null; + } + + private boolean sameText(String left, String right) + { + return StringUtils.defaultString(left).trim().equalsIgnoreCase(StringUtils.defaultString(right).trim()); + } + + private int productScore(AgentPlan plan, String originalText, ProductInfo product) + { + int score = 0; + score += fieldScore(plan.getProductName(), product.getProductName(), 8); + score += fieldScore(plan.getBrand(), product.getBrand(), 4); + score += fieldScore(plan.getCategory(), product.getCategory(), 4); + score += fieldScore(plan.getSpec(), product.getSpec(), 4); + score += fieldScore(plan.getUnit(), product.getUnit(), 1); + String fullInfo = productFullText(product); + if (containsIgnoreCase(originalText, fullInfo) || containsIgnoreCase(fullInfo, originalText)) + { + score += 2; + } + else + { + score += containsField(originalText, product.getProductName()) ? 4 : 0; + score += containsField(originalText, product.getBrand()) ? 2 : 0; + score += containsField(originalText, product.getCategory()) ? 2 : 0; + score += containsField(originalText, product.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 boolean matchesText(String input, String... values) + { + if (StringUtils.isBlank(input)) + { + return false; + } + for (String value : values) + { + if (containsField(input, value)) + { + 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)); + } + + private void fillProduct(AgentPlan plan, ProductInfo product) + { + plan.setProductId(product.getId()); + plan.setProductName(product.getProductName()); + plan.setBrand(product.getBrand()); + plan.setCategory(product.getCategory()); + plan.setSpec(product.getSpec()); + plan.setUnit(product.getUnit()); + } + + private String productFullText(ProductInfo product) + { + return StringUtils.defaultString(product.getProductName()) + " " + + StringUtils.defaultString(product.getBrand()) + " " + + StringUtils.defaultString(product.getCategory()) + " " + + StringUtils.defaultString(product.getSpec()) + " " + + StringUtils.defaultString(product.getUnit()); + } + + private String productDetail(ProductInfo product) + { + List parts = new ArrayList<>(); + addPart(parts, product.getBrand()); + addPart(parts, product.getCategory()); + addPart(parts, product.getSpec()); + addPart(parts, product.getUnit()); + return String.join(" / ", parts); + } + + private void addPart(List parts, String value) + { + if (StringUtils.isNotBlank(value)) + { + parts.add(value); + } + } + + private void validateQuantity(AgentPlan plan) + { + if (!validQuantity(plan)) + { + plan.getWarnings().add(ACTION_CREATE_CHECK.equals(plan.getAction()) ? "未识别到实际盘点数量" : "未识别到有效数量"); + } + } + + private boolean validQuantity(AgentPlan plan) + { + if (plan.getQuantity() == null) + { + return false; + } + if (ACTION_CREATE_CHECK.equals(plan.getAction())) + { + return plan.getQuantity().signum() >= 0; + } + return plan.getQuantity().signum() > 0; + } + + private void fillStockResults(AgentPlan plan) + { + StockInfo query = new StockInfo(); + query.setAreaId(plan.getAreaId()); + query.setProductId(plan.getProductId()); + if (plan.getProductId() == null) + { + query.setProductName(firstNotBlank(plan.getProductName(), plan.getBrand(), plan.getCategory(), plan.getSpec())); + } + List stockList = stockInfoService.selectStockInfoList(query); + int count = 0; + for (StockInfo stock : stockList) + { + if (count++ >= 20) + { + plan.getWarnings().add("结果较多,仅展示前 20 条"); + break; + } + plan.getResults().add(toResult(stock)); + } + } + + private AgentResultItem toResult(StockInfo stock) + { + AgentResultItem item = new AgentResultItem(); + item.setId(stock.getId()); + item.setAreaId(stock.getAreaId()); + item.setProductId(stock.getProductId()); + item.setAreaName(stock.getAreaName()); + item.setProductName(stock.getProductName()); + item.setBrand(stock.getBrand()); + item.setCategory(stock.getCategory()); + item.setSpec(stock.getSpec()); + item.setUnit(stock.getUnit()); + item.setStockNum(stock.getStockNum()); + return item; + } + + private void fillCurrentStock(AgentPlan plan) + { + if (plan.getAreaId() == null || plan.getProductId() == null) + { + return; + } + StockInfo query = new StockInfo(); + query.setAreaId(plan.getAreaId()); + query.setProductId(plan.getProductId()); + List stockList = stockInfoService.selectStockInfoList(query); + BigDecimal current = stockList.isEmpty() || stockList.get(0).getStockNum() == null ? BigDecimal.ZERO : stockList.get(0).getStockNum(); + plan.setCurrentStock(current); + if (plan.getQuantity() != null) + { + if (ACTION_CREATE_IN.equals(plan.getAction())) + { + plan.setAfterStock(current.add(plan.getQuantity())); + } + else if (ACTION_CREATE_OUT.equals(plan.getAction())) + { + plan.setAfterStock(current.subtract(plan.getQuantity())); + if (current.compareTo(plan.getQuantity()) < 0) + { + plan.setExecutable(false); + plan.setMessage("库存不足,当前库存:" + current); + } + } + else if (ACTION_CREATE_CHECK.equals(plan.getAction())) + { + plan.setAfterStock(plan.getQuantity()); + } + } + } + + private void suggestAlternativeStockAreas(AgentPlan plan) + { + if (!ACTION_CREATE_OUT.equals(plan.getAction()) || plan.getProductId() == null || plan.isExecutable()) + { + return; + } + StockInfo query = new StockInfo(); + query.setProductId(plan.getProductId()); + List stockList = stockInfoService.selectStockInfoList(query); + for (StockInfo stock : stockList) + { + if (stock.getStockNum() != null && stock.getStockNum().signum() > 0 + && (plan.getAreaId() == null || !stock.getAreaId().equals(plan.getAreaId()))) + { + plan.getResults().add(toResult(stock)); + } + if (plan.getResults().size() >= 10) + { + break; + } + } + if (!plan.getResults().isEmpty()) + { + plan.setNeedConfirm(false); + plan.setExecutable(false); + plan.setMessage("当前库区库存不足或没有库存,其他库区有库存。请说“第几个出库多少个”。"); + } + } + + private void fillSummary(AgentPlan plan) + { + if (StringUtils.isNotBlank(plan.getSummary())) + { + return; + } + String product = StringUtils.defaultIfBlank(plan.getProductName(), "未识别产品"); + String area = StringUtils.defaultIfBlank(plan.getAreaName(), "未识别库区"); + String quantity = plan.getQuantity() == null ? "未识别数量" : plan.getQuantity().stripTrailingZeros().toPlainString(); + if (ACTION_CREATE_IN.equals(plan.getAction())) + { + plan.setSummary("登记入库:" + area + "," + product + ",数量 " + quantity); + } + else if (ACTION_CREATE_OUT.equals(plan.getAction())) + { + plan.setSummary("执行出库:" + area + "," + product + ",数量 " + quantity); + } + else if (ACTION_CREATE_CHECK.equals(plan.getAction())) + { + plan.setSummary("登记盘库:" + area + "," + product + ",实际数量 " + quantity); + } + else if (ACTION_CREATE_PRODUCT.equals(plan.getAction())) + { + plan.setSummary("添加产品:" + product); + } + else if (ACTION_CREATE_AREA.equals(plan.getAction())) + { + plan.setSummary("添加库区:" + StringUtils.defaultIfBlank(plan.getAreaName(), "未识别库区")); + } + } + + private void resolveInRecord(AgentPlan plan, String originalText) + { + if (StringUtils.isBlank(plan.getRecordNo())) + { + plan.setRecordNo(extractInNo(originalText)); + } + if (StringUtils.isBlank(plan.getRecordNo())) + { + plan.setExecutable(false); + plan.setNeedConfirm(false); + plan.setMessage("请提供要确认的入库单号"); + return; + } + InRecord query = new InRecord(); + query.setInNo(plan.getRecordNo()); + List records = inRecordService.selectInRecordList(query); + if (records.isEmpty()) + { + plan.setExecutable(false); + plan.setNeedConfirm(false); + plan.setMessage("未找到入库单:" + plan.getRecordNo()); + return; + } + InRecord record = records.get(0); + plan.setRecordId(record.getId()); + plan.setRecordNo(record.getInNo()); + plan.setAreaId(record.getAreaId()); + plan.setProductId(record.getProductId()); + plan.setAreaName(record.getAreaName()); + plan.setProductName(record.getProductName()); + plan.setBrand(record.getBrand()); + plan.setCategory(record.getCategory()); + plan.setSpec(record.getSpec()); + plan.setUnit(record.getUnit()); + plan.setQuantity(record.getInNum()); + plan.setNeedConfirm(true); + plan.setExecutable(!Integer.valueOf(1).equals(record.getInStatus())); + plan.setSummary("确认入库:" + record.getInNo()); + plan.setMessage(plan.isExecutable() ? "请确认是否执行入库" : "该入库单已经确认"); + } + + private String resolveNickName(String username) + { + if (StringUtils.isBlank(username)) + { + return username; + } + SysUser user = userService.selectUserByUserName(username); + if (user != null && StringUtils.isNotBlank(user.getNickName())) + { + return user.getNickName(); + } + return username; + } + + private String firstNotBlank(String... values) + { + for (String value : values) + { + if (StringUtils.isNotBlank(value)) + { + return value; + } + } + return null; + } + + private List first(List source, int limit) + { + if (source.size() <= limit) + { + return source; + } + return source.subList(0, limit); + } + + private static class ScoredProduct + { + private final ProductInfo product; + private final int score; + + private ScoredProduct(ProductInfo product, int score) + { + this.product = product; + this.score = score; + } + + public ProductInfo getProduct() + { + return product; + } + + public int getScore() + { + return score; + } + } +} diff --git a/ruoyi-ui/src/api/warehouse/agent.js b/ruoyi-ui/src/api/warehouse/agent.js new file mode 100644 index 0000000..74fc455 --- /dev/null +++ b/ruoyi-ui/src/api/warehouse/agent.js @@ -0,0 +1,21 @@ +import request from '@/utils/request' + +export function analyzeAgent(data) { + return request({ + url: '/warehouse/agent/analyze', + method: 'post', + data, + headers: { + repeatSubmit: false + }, + timeout: 45000 + }) +} + +export function executeAgent(data) { + return request({ + url: '/warehouse/agent/execute', + method: 'post', + data + }) +} diff --git a/ruoyi-ui/src/views/index.vue b/ruoyi-ui/src/views/index.vue index cbf880f..f13d287 100644 --- a/ruoyi-ui/src/views/index.vue +++ b/ruoyi-ui/src/views/index.vue @@ -8,6 +8,68 @@ 移动端入口 +
+
+
+

智能库存助手

+

输入一句话,系统先生成操作草稿,确认后再真正执行。

+
+ 第一阶段 +
+
+ + 清空 + 分析 +
+
+
+
+ {{ actionLabel(agent.plan.action) }} + {{ agent.plan.summary || agent.plan.message || '已生成操作草稿' }} +
+ 确认执行 +
+ + {{ item.value }} + + +
+ {{ item }} +
+
+ {{ item.name }} {{ item.detail || '' }} +
+ + + + + + + + +
+
+
库存品项 @@ -85,6 +147,7 @@ import { listStock } from '@/api/warehouse/stock' import { listInRecord } from '@/api/warehouse/inRecord' import { listOutRecord } from '@/api/warehouse/outRecord' import { listStockCheck } from '@/api/warehouse/stockCheck' +import { analyzeAgent, executeAgent } from '@/api/warehouse/agent' export default { name: 'Index', @@ -96,13 +159,88 @@ export default { pendingCheck: 0, outTotal: 0 }, - stockList: [] + stockList: [], + agent: { + text: '', + loading: false, + executing: false, + contextResults: [], + plan: null + } + } + }, + computed: { + planItems() { + const plan = this.agent.plan + if (!plan) { + return [] + } + return [ + { label: '库区', value: plan.areaName }, + { label: '产品', value: plan.productName }, + { label: '品牌', value: plan.brand }, + { label: '类别', value: plan.category }, + { label: '规格', value: plan.spec }, + { label: '单位', value: plan.unit }, + { label: '数量', value: plan.quantity }, + { label: '操作人', value: plan.operatorName }, + { label: '当前库存', value: plan.currentStock }, + { label: '预计库存', value: plan.afterStock }, + { label: '单号', value: plan.recordNo } + ].filter(item => item.value !== undefined && item.value !== null && item.value !== '') } }, created() { this.loadDashboard() }, methods: { + analyzeAgentText() { + const text = (this.agent.text || '').trim() + if (!text) { + this.$modal.msgWarning('请输入要操作的内容') + return + } + this.agent.loading = true + analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => { + this.agent.plan = res.data + this.agent.text = '' + if (res.data && res.data.results && res.data.results.length) { + this.agent.contextResults = res.data.results + } + }).finally(() => { + this.agent.loading = false + }) + }, + executeAgentPlan() { + if (!this.agent.plan || !this.agent.plan.executable) { + return + } + this.agent.executing = true + executeAgent({ plan: this.agent.plan }).then(res => { + this.agent.plan = res.data + this.$modal.msgSuccess(res.data.message || '操作完成') + this.loadDashboard() + this.agent.text = '' + }).finally(() => { + this.agent.executing = false + }) + }, + clearAgent() { + this.agent.text = '' + this.agent.plan = null + }, + actionLabel(action) { + const map = { + query_stock: '查询库存', + create_in_record: '入库登记', + confirm_in_record: '确认入库', + create_out_record: '出库', + create_stock_check: '盘库登记', + create_product: '新增产品', + create_area: '新增库区' + } + return map[action] || '未识别' + }, loadDashboard() { listStock({ pageNum: 1, pageSize: 8 }).then(res => { this.stockList = res.rows || [] @@ -150,6 +288,76 @@ export default { margin: 6px 0 0; color: #7a8794; } +.agent-panel { + margin-bottom: 16px; + padding: 18px; + background: #ffffff; + border: 1px solid #e6ebf2; + border-radius: 8px; +} +.agent-title { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 12px; +} +.agent-title h2 { + margin: 0; + font-size: 18px; + line-height: 26px; + color: #1f2d3d; +} +.agent-title p { + margin: 4px 0 0; + color: #7a8794; +} +.agent-input { + display: grid; + grid-template-columns: 1fr 88px 96px; + gap: 12px; + align-items: stretch; +} +.agent-input .el-button { + height: 54px; +} +.agent-result { + display: grid; + gap: 12px; + margin-top: 14px; +} +.agent-plan { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px; + background: #f7f9fc; + border-radius: 6px; +} +.agent-plan strong { + margin-left: 8px; + color: #1f2d3d; +} +.agent-warnings { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.agent-candidates { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.agent-candidates span { + padding: 6px 10px; + color: #606266; + background: #f4f6f9; + border-radius: 4px; +} +.agent-table { + width: 100%; +} .summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -222,6 +430,16 @@ export default { align-items: flex-start; flex-direction: column; } + .agent-input { + grid-template-columns: 1fr; + } + .agent-input .el-button { + height: 40px; + } + .agent-plan { + align-items: flex-start; + flex-direction: column; + } .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } diff --git a/ruoyi-ui/src/views/mobile/index.vue b/ruoyi-ui/src/views/mobile/index.vue index ac2f0f6..6e917f3 100644 --- a/ruoyi-ui/src/views/mobile/index.vue +++ b/ruoyi-ui/src/views/mobile/index.vue @@ -29,6 +29,63 @@
+
+
+
+
智能库存助手
+
说一句或输入一句,确认后执行
+
+ AI +
+ +
+ 清空 + 分析 +
+
+
+
+ {{ actionLabel(agent.plan.action) }} + {{ agent.plan.summary || agent.plan.message || '已生成操作草稿' }} +
+ 确认 +
+
+ {{ item.label }}:{{ item.value }} +
+ +
+ {{ item }} +
+
+ {{ item.name }} {{ item.detail || '' }} +
+
+
+
第 {{ index + 1 }} 项:{{ item.productName || '-' }}
+
{{ item.areaName || '-' }} | {{ productMeta(item) }}
+
+ {{ item.stockNum }} +
+
+
+
@@ -108,6 +165,8 @@ 入库人:{{ item.inOperator || '-' }}
+ 修改 + 删除 确认入库
@@ -372,9 +431,10 @@ import { listArea, getArea, delArea, addArea, updateArea, optionselectArea } fro import { listProduct, getProduct, delProduct, addProduct, updateProduct, optionselectProduct } from '@/api/warehouse/product' import { optionselectUser } from '@/api/warehouse/user' import { listStock, outStock } from '@/api/warehouse/stock' -import { listInRecord, addInRecord, confirmInRecord } from '@/api/warehouse/inRecord' +import { listInRecord, getInRecord, addInRecord, updateInRecord, delInRecord, confirmInRecord } from '@/api/warehouse/inRecord' import { listOutRecord, addOutRecord } from '@/api/warehouse/outRecord' import { listStockCheck, addStockCheck, confirmStockCheck } from '@/api/warehouse/stockCheck' +import { analyzeAgent, executeAgent } from '@/api/warehouse/agent' export default { name: 'MobileWarehouse', @@ -402,7 +462,15 @@ export default { dialogType: '', dialogTitle: '', form: {}, - formRules: {} + formRules: {}, + agent: { + text: '', + loading: false, + executing: false, + contextResults: [], + pendingNextPlan: null, + plan: null + } } }, computed: { @@ -415,6 +483,25 @@ export default { }, unitOptions() { return this.distinctProductOptions('unit') + }, + agentPlanItems() { + const plan = this.agent.plan + if (!plan) { + return [] + } + return [ + { label: '库区', value: plan.areaName }, + { label: '产品', value: plan.productName }, + { label: '品牌', value: plan.brand }, + { label: '类别', value: plan.category }, + { label: '规格', value: plan.spec }, + { label: '单位', value: plan.unit }, + { label: '数量', value: plan.quantity }, + { label: '人员', value: plan.operatorName }, + { label: '当前', value: plan.currentStock }, + { label: '预计', value: plan.afterStock }, + { label: '单号', value: plan.recordNo } + ].filter(item => item.value !== undefined && item.value !== null && item.value !== '') } }, created() { @@ -422,6 +509,158 @@ export default { this.refreshAll() }, methods: { + analyzeAgentText() { + const text = (this.agent.text || '').trim() + if (!text) { + this.$modal.msgWarning('请输入要操作的内容') + return + } + this.agent.loading = true + analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => { + this.agent.plan = res.data + this.agent.text = '' + if (res.data && res.data.results && res.data.results.length) { + this.agent.contextResults = res.data.results + } + this.autoFillAgentForm(res.data) + }).finally(() => { + this.agent.loading = false + }) + }, + executeAgentPlan() { + if (!this.agent.plan || !this.agent.plan.executable) { + return + } + this.agent.executing = true + executeAgent({ plan: this.agent.plan }).then(res => { + this.agent.plan = res.data + this.$modal.msgSuccess(res.data.message || '操作完成') + this.refreshAll() + this.agent.text = '' + }).finally(() => { + this.agent.executing = false + }) + }, + clearAgent() { + this.agent.text = '' + this.agent.plan = null + this.agent.pendingNextPlan = null + }, + autoFillAgentForm(plan) { + if (!plan || !plan.executable) { + return + } + if (plan.action === 'create_out_record' && plan.stockId) { + this.dialogType = 'stockOut' + this.dialogTitle = '库存出库' + this.form = { + id: plan.stockId, + productName: plan.productName, + brand: plan.brand, + category: plan.category, + spec: plan.spec, + unit: plan.unit, + outOperator: plan.operatorName, + outType: plan.outType || 3, + outNum: plan.quantity, + sourceNo: undefined, + remark: plan.remark + } + this.setOutRules() + } else if (plan.action === 'create_out_record') { + this.dialogType = 'out' + this.dialogTitle = '出库登记' + this.form = { + areaId: plan.areaId, + productId: plan.productId, + outOperator: plan.operatorName, + outType: plan.outType || 3, + outNum: plan.quantity, + sourceNo: undefined, + remark: plan.remark + } + this.setOutRules() + } else if (plan.action === 'create_in_record') { + this.dialogType = 'in' + this.dialogTitle = '入库登记' + this.form = { + areaId: plan.areaId, + productId: plan.productId, + inOperator: plan.operatorName, + inType: plan.inType || 2, + inNum: plan.quantity, + remark: plan.remark + } + this.setInRules() + } else if (plan.action === 'create_stock_check') { + this.dialogType = 'check' + this.dialogTitle = '盘库登记' + this.form = { + areaId: plan.areaId, + productId: plan.productId, + realNum: plan.quantity, + reason: plan.remark + } + this.setCheckRules() + } else if (plan.action === 'create_product') { + this.dialogType = 'product' + this.dialogTitle = '新增产品' + this.agent.pendingNextPlan = plan.nextPlan || null + this.form = { + id: undefined, + productCode: undefined, + productName: plan.productName, + brand: plan.brand, + category: plan.category, + spec: plan.spec, + unit: plan.unit, + remark: plan.remark + } + this.formRules = { + productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }] + } + } else if (plan.action === 'create_area') { + this.dialogType = 'area' + this.dialogTitle = '新增库区' + this.agent.pendingNextPlan = plan.nextPlan || null + this.form = { + id: undefined, + areaCode: undefined, + areaName: plan.areaName, + remark: plan.remark + } + this.formRules = { + areaName: [{ required: true, message: '请输入库区名称', trigger: 'blur' }] + } + } else { + return + } + this.dialogOpen = true + this.$nextTick(() => { + if (this.$refs.mobileForm) { + this.$refs.mobileForm.clearValidate() + } + }) + this.$modal.msgSuccess('已自动填好表单,请确认后提交') + }, + showAgentExecute(plan) { + if (!plan || !plan.needConfirm) { + return false + } + return !['create_out_record', 'create_in_record', 'create_stock_check', 'create_product', 'create_area'].includes(plan.action) + }, + actionLabel(action) { + const map = { + query_stock: '查库存', + create_in_record: '入库', + confirm_in_record: '确认入库', + create_out_record: '出库', + create_stock_check: '盘库', + create_product: '新增产品', + create_area: '新增库区' + } + return map[action] || '未识别' + }, loadOptions() { optionselectArea().then(res => { this.areaOptions = res.data || [] }) optionselectProduct().then(res => { this.productOptions = res.data || [] }) @@ -489,10 +728,31 @@ export default { const values = this.productOptions.map(item => item[field]).filter(item => item !== undefined && item !== null && item !== '') return Array.from(new Set(values)) }, - openInForm() { + openInForm(row) { this.dialogType = 'in' - this.dialogTitle = '入库登记' - this.form = { areaId: undefined, productId: undefined, inOperator: undefined, inType: 1, inNum: undefined, remark: undefined } + this.setInRules() + const setForm = data => { + this.dialogTitle = data && data.id ? '修改入库登记' : '入库登记' + this.form = Object.assign({ + id: undefined, + inNo: undefined, + areaId: undefined, + productId: undefined, + inOperator: undefined, + inType: 1, + inNum: undefined, + remark: undefined + }, data || {}) + this.dialogOpen = true + this.$nextTick(() => this.resetForm('mobileForm')) + } + if (row && row.id) { + getInRecord(row.id).then(response => setForm(response.data)) + } else { + setForm() + } + }, + setInRules() { this.formRules = { areaId: [{ required: true, message: '请选择库区', trigger: 'change' }], productId: [{ required: true, message: '请选择产品', trigger: 'change' }], @@ -500,8 +760,6 @@ export default { inType: [{ required: true, message: '请选择类型', trigger: 'change' }], inNum: [{ required: true, message: '请输入数量', trigger: 'blur' }] } - this.dialogOpen = true - this.$nextTick(() => this.resetForm('mobileForm')) }, openOutForm() { this.dialogType = 'out' @@ -532,13 +790,62 @@ export default { this.dialogType = 'check' this.dialogTitle = '盘库登记' this.form = { areaId: undefined, productId: undefined, realNum: undefined, reason: undefined } + this.setCheckRules() + this.dialogOpen = true + this.$nextTick(() => this.resetForm('mobileForm')) + }, + setCheckRules() { this.formRules = { areaId: [{ required: true, message: '请选择库区', trigger: 'change' }], productId: [{ required: true, message: '请选择产品', trigger: 'change' }], realNum: [{ required: true, message: '请输入实盘数量', trigger: 'blur' }] } - this.dialogOpen = true - this.$nextTick(() => this.resetForm('mobileForm')) + }, + openPendingAgentPlanAfterProduct() { + const nextPlan = this.agent.pendingNextPlan + const savedProduct = Object.assign({}, this.form) + this.agent.pendingNextPlan = null + optionselectProduct().then(res => { + this.productOptions = res.data || [] + const product = this.productOptions.find(item => + this.sameText(item.productName, savedProduct.productName) && + this.sameText(item.brand, savedProduct.brand) && + this.sameText(item.category, savedProduct.category) && + this.sameText(item.spec, savedProduct.spec) + ) + if (!product) { + this.$modal.msgWarning('产品已保存,请手动选择产品完成入库') + return + } + nextPlan.productId = product.id + nextPlan.productName = product.productName + nextPlan.brand = product.brand + nextPlan.category = product.category + nextPlan.spec = product.spec + nextPlan.unit = product.unit + nextPlan.executable = true + this.$nextTick(() => this.autoFillAgentForm(nextPlan)) + }) + }, + openPendingAgentPlanAfterArea() { + const nextPlan = this.agent.pendingNextPlan + const savedArea = Object.assign({}, this.form) + this.agent.pendingNextPlan = null + optionselectArea().then(res => { + this.areaOptions = res.data || [] + const area = this.areaOptions.find(item => this.sameText(item.areaName, savedArea.areaName)) + if (!area) { + this.$modal.msgWarning('库区已保存,请手动选择库区完成操作') + return + } + nextPlan.areaId = area.id + nextPlan.areaName = area.areaName + nextPlan.executable = true + this.$nextTick(() => this.autoFillAgentForm(nextPlan)) + }) + }, + sameText(left, right) { + return String(left || '').trim().toLowerCase() === String(right || '').trim().toLowerCase() }, openProductForm(row) { this.dialogType = 'product' @@ -592,7 +899,7 @@ export default { this.$refs.mobileForm.validate(valid => { if (!valid) return let request - if (this.dialogType === 'in') request = addInRecord(this.form) + if (this.dialogType === 'in') request = this.form.id ? updateInRecord(this.form) : addInRecord(this.form) if (this.dialogType === 'out') request = addOutRecord(this.form) if (this.dialogType === 'stockOut') request = outStock(this.form) if (this.dialogType === 'check') request = addStockCheck(this.form) @@ -600,6 +907,7 @@ export default { if (this.dialogType === 'area') request = this.form.id ? updateArea(this.form) : addArea(this.form) request.then(() => { const editMaintainData = (this.dialogType === 'product' || this.dialogType === 'area') && this.form.id + const hasPendingNextPlan = !!this.agent.pendingNextPlan this.$modal.msgSuccess(editMaintainData ? '修改成功' : '保存成功') this.dialogOpen = false if (this.dialogType === 'product') { @@ -607,6 +915,11 @@ export default { this.loadOptions() this.loadStock() this.loadSummary() + if (hasPendingNextPlan && !editMaintainData) { + this.openPendingAgentPlanAfterProduct() + } else { + this.clearAgent() + } return } if (this.dialogType === 'area') { @@ -614,8 +927,14 @@ export default { this.loadOptions() this.loadStock() this.loadSummary() + if (hasPendingNextPlan && !editMaintainData) { + this.openPendingAgentPlanAfterArea() + } else { + this.clearAgent() + } return } + this.clearAgent() this.refreshAll() }) }) @@ -638,6 +957,13 @@ export default { this.loadSummary() }).catch(() => {}) }, + deleteInItem(row) { + this.$modal.confirm('确认删除入库记录“' + (row.inNo || '-') + '”?').then(() => delInRecord(row.id)).then(() => { + this.$modal.msgSuccess('删除成功') + this.loadInRecords() + this.loadSummary() + }).catch(() => {}) + }, confirmIn(row) { this.$modal.confirm('确认将该记录入库?').then(() => confirmInRecord(row.id)).then(() => { this.$modal.msgSuccess('入库成功') @@ -715,6 +1041,98 @@ export default { .summary-card.danger strong { color: #f56c6c; } +.mobile-agent { + margin: 12px 0; + padding: 12px; + background: #ffffff; + border: 1px solid #e8edf3; + border-radius: 8px; +} +.agent-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; +} +.agent-title { + font-size: 16px; + font-weight: 700; + line-height: 22px; +} +.agent-subtitle { + color: #7a8794; + font-size: 12px; + line-height: 18px; +} +.agent-actions { + display: grid; + grid-template-columns: 86px 1fr; + gap: 8px; + margin-top: 8px; +} +.agent-result { + display: grid; + gap: 10px; + margin-top: 10px; +} +.agent-plan { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + padding: 10px; + background: #f7f9fc; + border-radius: 6px; +} +.agent-plan strong { + display: block; + margin-top: 5px; + line-height: 20px; +} +.agent-detail { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.agent-detail span { + padding: 5px 8px; + color: #606266; + background: #f4f6f9; + border-radius: 4px; + font-size: 12px; +} +.agent-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.agent-candidates { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.agent-candidates span { + padding: 5px 8px; + color: #606266; + background: #f4f6f9; + border-radius: 4px; + font-size: 12px; +} +.agent-stock { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + padding: 10px; + border: 1px solid #e8edf3; + border-radius: 6px; +} +.agent-stock strong { + color: #1f7aec; + font-size: 18px; + white-space: nowrap; +} .quick-panel { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -874,4 +1292,19 @@ export default { .mobile-dialog .el-dialog__body { padding: 12px 16px 0; } +@media (max-width: 768px) { + .el-message-box__wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + box-sizing: border-box; + } + .el-message-box { + width: 100% !important; + max-width: 340px; + margin-top: 0 !important; + margin-bottom: 0; + } +}