Compare commits

..

4 Commits
master ... dev

Author SHA1 Message Date
46ec60d6f5 feat(warehouse): 优化库存搜索功能支持关键词分词匹配
- 将产品信息搜索字段改为关键词搜索,支持输入多个条件
- 实现分词搜索算法,支持按名称/规格/品牌/类别/单位/库区等多维度匹配
- 添加搜索结果相关性评分排序机制
- 优化移动端和PC端搜索界面提示文案
- 实现关键词高亮显示和精确/前缀/包含匹配策略
- 添加最大分词数量限制防止过度匹配
- 重构后端分页处理逻辑以适应新的搜索模式
2026-07-27 09:57:42 +08:00
e76c07fb5e feat(warehouse): 新增批量导入功能并优化系统配置
- 添加批量导入页面支持Excel和图片上传解析
- 实现移动端批量导入功能界面和交互
- 集成图片压缩上传功能提升用户体验
- 增加批量任务状态管理和进度跟踪
- 完善批量数据确认和执行流程
- 配置环境变量支持Docker部署
- 提高文件上传大小限制增强实用性
- 优化系统配置以适应生产环境需求
2026-07-17 15:00:06 +08:00
d872e3e235 feat(warehouse): 优化智能代理功能和库存管理
- 修改默认出库类型从销售出库改为生产出库
- 添加产品类别自动推断功能,支持基于历史数据和关键词的智能分类
- 实现库存查询结果显示和选择功能,优化用户体验
- 增强代理计划的消息提示和错误处理机制
- 添加现有产品类别的获取和展示功能
- 优化库存不足和产品未找到的提示信息
- 修复前端表单默认值设置问题
2026-07-17 09:17:42 +08:00
7737fda061 feat(wms): 添加智能库存助手功能
- 集成 OpenAI 模型配置,支持自然语言库存操作
- 在移动端界面添加智能助手交互组件
- 实现库存操作计划生成与执行流程
- 添加入库登记的修改和删除功能
- 新增 agent 相关 API 接口和数据模型
- 支持上下文关联和连续操作处理
- 提供操作预览和确认机制
- 完善前后端交互和状态管理逻辑
2026-07-16 14:16:55 +08:00
34 changed files with 6206 additions and 24 deletions

View File

@ -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()));
}
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.web.controller.warehouse; package com.ruoyi.web.controller.warehouse;
import java.util.Collections;
import java.util.List; import java.util.List;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -11,10 +12,14 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.warehouse.domain.OutRecord; import com.ruoyi.warehouse.domain.OutRecord;
import com.ruoyi.warehouse.domain.StockInfo; import com.ruoyi.warehouse.domain.StockInfo;
@ -31,10 +36,31 @@ public class StockInfoController extends BaseController
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(StockInfo stockInfo) public TableDataInfo list(StockInfo stockInfo)
{ {
if (StringUtils.isNotBlank(stockInfo.getProductName()))
{
return getKeywordDataTable(stockInfoService.selectStockInfoList(stockInfo));
}
startPage(); startPage();
return getDataTable(stockInfoService.selectStockInfoList(stockInfo)); return getDataTable(stockInfoService.selectStockInfoList(stockInfo));
} }
private TableDataInfo getKeywordDataTable(List<StockInfo> list)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
int pageNum = pageDomain.getPageNum() == null || pageDomain.getPageNum() < 1 ? 1 : pageDomain.getPageNum();
int pageSize = pageDomain.getPageSize() == null || pageDomain.getPageSize() < 1 ? 10 : pageDomain.getPageSize();
int total = list == null ? 0 : list.size();
int fromIndex = Math.min((pageNum - 1) * pageSize, total);
int toIndex = Math.min(fromIndex + pageSize, total);
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setTotal(total);
rspData.setRows(total == 0 ? Collections.emptyList() : list.subList(fromIndex, toIndex));
return rspData;
}
@PreAuthorize("@ss.hasPermi('warehouse:stock:export')") @PreAuthorize("@ss.hasPermi('warehouse:stock:export')")
@Log(title = "库存信息", businessType = BusinessType.EXPORT) @Log(title = "库存信息", businessType = BusinessType.EXPORT)
@PostMapping("/export") @PostMapping("/export")

View File

@ -0,0 +1,80 @@
package com.ruoyi.web.controller.warehouse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.warehouse.domain.WmsBatchActionUpdateRequest;
import com.ruoyi.warehouse.domain.WmsBatchItem;
import com.ruoyi.warehouse.domain.WmsBatchJob;
import com.ruoyi.warehouse.service.IWmsBatchImportService;
@RestController
@RequestMapping("/warehouse/batch")
public class WmsBatchImportController extends BaseController
{
@Autowired
private IWmsBatchImportService batchImportService;
@GetMapping("/list")
public TableDataInfo list(WmsBatchJob job)
{
startPage();
return getDataTable(batchImportService.selectJobList(job));
}
@PostMapping("/upload")
@Log(title = "WMS批量导入", businessType = BusinessType.IMPORT)
public AjaxResult upload(MultipartFile file) throws Exception
{
return success(batchImportService.uploadExcel(file, getUsername()));
}
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id)
{
return success(batchImportService.selectJobById(id));
}
@GetMapping("/{jobId}/items")
public AjaxResult items(@PathVariable Long jobId)
{
return success(batchImportService.selectItemList(jobId));
}
@PutMapping("/item")
public AjaxResult updateItem(@RequestBody WmsBatchItem item)
{
return success(batchImportService.updateItem(item));
}
@PutMapping("/{jobId}/items/action")
public AjaxResult updateItemAction(@PathVariable Long jobId, @RequestBody WmsBatchActionUpdateRequest request)
{
return toAjax(batchImportService.updateItemAction(jobId, request.getActionType(), request.getItemIds()));
}
@DeleteMapping("/item/{id}")
public AjaxResult deleteItem(@PathVariable Long id)
{
return toAjax(batchImportService.deleteItem(id));
}
@PostMapping("/{jobId}/confirm")
@Log(title = "WMS批量确认执行", businessType = BusinessType.INSERT)
public AjaxResult confirm(@PathVariable Long jobId)
{
return success(batchImportService.confirmJob(jobId, getUsername()));
}
}

View File

@ -6,13 +6,12 @@ ruoyi:
version: 3.9.2 version: 3.9.2
# 版权年份 # 版权年份
copyrightYear: 2026 copyrightYear: 2026
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath # 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux/Docker配置 /app/uploadPath
profile: D:/ruoyi/uploadPath profile: ${RUOYI_PROFILE:D:/ruoyi/uploadPath}
# 获取ip地址开关 # 获取ip地址开关
addressEnabled: false addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证 # 验证码类型 math 数字计算 char 字符验证
captchaType: math captchaType: math
# 开发环境配置 # 开发环境配置
server: server:
# 服务器的HTTP端口默认为8080 # 服务器的HTTP端口默认为8080
@ -57,9 +56,9 @@ spring:
servlet: servlet:
multipart: multipart:
# 单个文件大小 # 单个文件大小
max-file-size: 10MB max-file-size: ${MAX_FILE_SIZE:50MB}
# 设置总上传的文件大小 # 设置总上传的文件大小
max-request-size: 20MB max-request-size: ${MAX_REQUEST_SIZE:60MB}
# 服务模块 # 服务模块
devtools: devtools:
restart: restart:
@ -142,3 +141,10 @@ xss:
excludes: /system/notice excludes: /system/notice
# 匹配链接 # 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/* 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}

View File

@ -0,0 +1,29 @@
package com.ruoyi.warehouse.domain;
import java.util.List;
public class WmsBatchActionUpdateRequest
{
private String actionType;
private List<Long> itemIds;
public String getActionType()
{
return actionType;
}
public void setActionType(String actionType)
{
this.actionType = actionType;
}
public List<Long> getItemIds()
{
return itemIds;
}
public void setItemIds(List<Long> itemIds)
{
this.itemIds = itemIds;
}
}

View File

@ -0,0 +1,74 @@
package com.ruoyi.warehouse.domain;
import java.math.BigDecimal;
import com.ruoyi.common.core.domain.BaseEntity;
public class WmsBatchItem extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
private Long jobId;
private Integer rowNo;
private String actionType;
private String rawText;
private String parsedJson;
private Integer status;
private String errorMsg;
private String warningMsg;
private Long refId;
private String areaName;
private String productName;
private String brand;
private String category;
private String spec;
private String unit;
private BigDecimal quantity;
private String operatorName;
private Integer typeCode;
private String sourceNo;
private String remark;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getJobId() { return jobId; }
public void setJobId(Long jobId) { this.jobId = jobId; }
public Integer getRowNo() { return rowNo; }
public void setRowNo(Integer rowNo) { this.rowNo = rowNo; }
public String getActionType() { return actionType; }
public void setActionType(String actionType) { this.actionType = actionType; }
public String getRawText() { return rawText; }
public void setRawText(String rawText) { this.rawText = rawText; }
public String getParsedJson() { return parsedJson; }
public void setParsedJson(String parsedJson) { this.parsedJson = parsedJson; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getErrorMsg() { return errorMsg; }
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
public String getWarningMsg() { return warningMsg; }
public void setWarningMsg(String warningMsg) { this.warningMsg = warningMsg; }
public Long getRefId() { return refId; }
public void setRefId(Long refId) { this.refId = refId; }
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 String getOperatorName() { return operatorName; }
public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
public Integer getTypeCode() { return typeCode; }
public void setTypeCode(Integer typeCode) { this.typeCode = typeCode; }
public String getSourceNo() { return sourceNo; }
public void setSourceNo(String sourceNo) { this.sourceNo = sourceNo; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
}

View File

@ -0,0 +1,141 @@
package com.ruoyi.warehouse.domain;
import com.ruoyi.common.core.domain.BaseEntity;
public class WmsBatchJob extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
private String jobNo;
private String sourceType;
private String bizType;
private String fileName;
private String filePath;
private Integer status;
private Integer progress;
private Integer totalCount;
private Integer successCount;
private Integer failCount;
private String errorMsg;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getJobNo()
{
return jobNo;
}
public void setJobNo(String jobNo)
{
this.jobNo = jobNo;
}
public String getSourceType()
{
return sourceType;
}
public void setSourceType(String sourceType)
{
this.sourceType = sourceType;
}
public String getBizType()
{
return bizType;
}
public void setBizType(String bizType)
{
this.bizType = bizType;
}
public String getFileName()
{
return fileName;
}
public void setFileName(String fileName)
{
this.fileName = fileName;
}
public String getFilePath()
{
return filePath;
}
public void setFilePath(String filePath)
{
this.filePath = filePath;
}
public Integer getStatus()
{
return status;
}
public void setStatus(Integer status)
{
this.status = status;
}
public Integer getProgress()
{
return progress;
}
public void setProgress(Integer progress)
{
this.progress = progress;
}
public Integer getTotalCount()
{
return totalCount;
}
public void setTotalCount(Integer totalCount)
{
this.totalCount = totalCount;
}
public Integer getSuccessCount()
{
return successCount;
}
public void setSuccessCount(Integer successCount)
{
this.successCount = successCount;
}
public Integer getFailCount()
{
return failCount;
}
public void setFailCount(Integer failCount)
{
this.failCount = failCount;
}
public String getErrorMsg()
{
return errorMsg;
}
public void setErrorMsg(String errorMsg)
{
this.errorMsg = errorMsg;
}
}

View File

@ -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<AgentResultItem> contextResults = new ArrayList<>();
public String getText()
{
return text;
}
public void setText(String text)
{
this.text = text;
}
public List<AgentResultItem> getContextResults()
{
return contextResults;
}
public void setContextResults(List<AgentResultItem> contextResults)
{
this.contextResults = contextResults;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<String> warnings = new ArrayList<>();
private List<AgentCandidate> candidates = new ArrayList<>();
private List<AgentResultItem> 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<String> getWarnings()
{
return warnings;
}
public void setWarnings(List<String> warnings)
{
this.warnings = warnings;
}
public List<AgentCandidate> getCandidates()
{
return candidates;
}
public void setCandidates(List<AgentCandidate> candidates)
{
this.candidates = candidates;
}
public List<AgentResultItem> getResults()
{
return results;
}
public void setResults(List<AgentResultItem> results)
{
this.results = results;
}
public AgentPlan getNextPlan()
{
return nextPlan;
}
public void setNextPlan(AgentPlan nextPlan)
{
this.nextPlan = nextPlan;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.warehouse.mapper;
import java.util.List;
import com.ruoyi.warehouse.domain.WmsBatchItem;
public interface WmsBatchItemMapper
{
public List<WmsBatchItem> selectWmsBatchItemList(WmsBatchItem item);
public WmsBatchItem selectWmsBatchItemById(Long id);
public int insertWmsBatchItem(WmsBatchItem item);
public int updateWmsBatchItem(WmsBatchItem item);
public int deleteWmsBatchItemById(Long id);
public int deleteWmsBatchItemByJobId(Long jobId);
public int countByJobIdAndStatus(WmsBatchItem item);
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.warehouse.mapper;
import java.util.List;
import com.ruoyi.warehouse.domain.WmsBatchJob;
public interface WmsBatchJobMapper
{
public List<WmsBatchJob> selectWmsBatchJobList(WmsBatchJob job);
public WmsBatchJob selectWmsBatchJobById(Long id);
public int insertWmsBatchJob(WmsBatchJob job);
public int updateWmsBatchJob(WmsBatchJob job);
}

View File

@ -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<AgentResultItem> contextResults, String username);
public AgentPlan execute(AgentPlan plan, String username);
}

View File

@ -9,6 +9,7 @@ public interface IProductInfoService
public List<ProductInfo> selectProductInfoAll(); public List<ProductInfo> selectProductInfoAll();
public ProductInfo selectProductInfoById(Long id); public ProductInfo selectProductInfoById(Long id);
public boolean checkProductCodeUnique(ProductInfo productInfo); public boolean checkProductCodeUnique(ProductInfo productInfo);
public String inferProductCategory(ProductInfo productInfo);
public int insertProductInfo(ProductInfo productInfo); public int insertProductInfo(ProductInfo productInfo);
public int updateProductInfo(ProductInfo productInfo); public int updateProductInfo(ProductInfo productInfo);
public int deleteProductInfoByIds(Long[] ids); public int deleteProductInfoByIds(Long[] ids);

View File

@ -0,0 +1,18 @@
package com.ruoyi.warehouse.service;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.warehouse.domain.WmsBatchItem;
import com.ruoyi.warehouse.domain.WmsBatchJob;
public interface IWmsBatchImportService
{
public WmsBatchJob uploadExcel(MultipartFile file, String username) throws Exception;
public List<WmsBatchJob> selectJobList(WmsBatchJob job);
public WmsBatchJob selectJobById(Long id);
public List<WmsBatchItem> selectItemList(Long jobId);
public WmsBatchItem updateItem(WmsBatchItem item);
public int updateItemAction(Long jobId, String actionType, List<Long> itemIds);
public int deleteItem(Long id);
public WmsBatchJob confirmJob(Long jobId, String username);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,9 @@
package com.ruoyi.warehouse.service.impl; package com.ruoyi.warehouse.service.impl;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.UserConstants; 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; 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 @Override
public int insertProductInfo(ProductInfo productInfo) public int insertProductInfo(ProductInfo productInfo)
{ {
@ -79,6 +97,10 @@ public class ProductInfoServiceImpl implements IProductInfoService
productInfo.setCategory(trimToNull(productInfo.getCategory())); productInfo.setCategory(trimToNull(productInfo.getCategory()));
productInfo.setSpec(trimToNull(productInfo.getSpec())); productInfo.setSpec(trimToNull(productInfo.getSpec()));
productInfo.setUnit(trimToNull(productInfo.getUnit())); productInfo.setUnit(trimToNull(productInfo.getUnit()));
if (StringUtils.isBlank(productInfo.getCategory()) && StringUtils.isNotBlank(productInfo.getProductName()))
{
productInfo.setCategory(inferProductCategory(productInfo));
}
} }
private void validateProductInfoUnique(ProductInfo productInfo) private void validateProductInfoUnique(ProductInfo productInfo)
@ -95,4 +117,151 @@ public class ProductInfoServiceImpl implements IProductInfoService
{ {
return StringUtils.isBlank(value) ? null : value.trim(); return StringUtils.isBlank(value) ? null : value.trim();
} }
private String inferCategoryFromHistory(ProductInfo productInfo)
{
Map<String, Integer> 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<String, Integer> 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));
}
} }

View File

@ -1,9 +1,12 @@
package com.ruoyi.warehouse.service.impl; package com.ruoyi.warehouse.service.impl;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.warehouse.domain.OutRecord; import com.ruoyi.warehouse.domain.OutRecord;
import com.ruoyi.warehouse.domain.StockInfo; import com.ruoyi.warehouse.domain.StockInfo;
import com.ruoyi.warehouse.mapper.StockInfoMapper; import com.ruoyi.warehouse.mapper.StockInfoMapper;
@ -13,6 +16,8 @@ import com.ruoyi.warehouse.service.IStockInfoService;
@Service @Service
public class StockInfoServiceImpl implements IStockInfoService public class StockInfoServiceImpl implements IStockInfoService
{ {
private static final int MAX_KEYWORD_TOKEN_COUNT = 5;
@Autowired @Autowired
private StockInfoMapper stockInfoMapper; private StockInfoMapper stockInfoMapper;
@ -22,7 +27,121 @@ public class StockInfoServiceImpl implements IStockInfoService
@Override @Override
public List<StockInfo> selectStockInfoList(StockInfo stockInfo) public List<StockInfo> selectStockInfoList(StockInfo stockInfo)
{ {
return stockInfoMapper.selectStockInfoList(stockInfo); List<String> tokens = parseKeywordTokens(stockInfo);
List<StockInfo> list = stockInfoMapper.selectStockInfoList(stockInfo);
if (tokens.isEmpty())
{
return list;
}
List<ScoredStock> scoredList = new ArrayList<>();
for (StockInfo item : list)
{
int score = scoreStock(item, tokens);
if (score > 0)
{
scoredList.add(new ScoredStock(item, score));
}
}
scoredList.sort((left, right) -> {
int scoreCompare = Integer.compare(right.score, left.score);
if (scoreCompare != 0)
{
return scoreCompare;
}
long leftId = left.stock.getId() == null ? 0L : left.stock.getId();
long rightId = right.stock.getId() == null ? 0L : right.stock.getId();
return Long.compare(rightId, leftId);
});
List<StockInfo> result = new ArrayList<>(scoredList.size());
for (ScoredStock item : scoredList)
{
result.add(item.stock);
}
return result;
}
private List<String> parseKeywordTokens(StockInfo stockInfo)
{
if (stockInfo == null || StringUtils.isBlank(stockInfo.getProductName()))
{
return new ArrayList<>();
}
LinkedHashSet<String> tokens = new LinkedHashSet<>();
String keyword = stockInfo.getProductName().trim().toLowerCase();
for (String item : keyword.split("[\\s,;;、]+"))
{
if (StringUtils.isNotBlank(item))
{
tokens.add(item.trim());
}
if (tokens.size() >= MAX_KEYWORD_TOKEN_COUNT)
{
break;
}
}
if (tokens.isEmpty())
{
return new ArrayList<>();
}
stockInfo.getParams().put("keywordTokens", new ArrayList<>(tokens));
return new ArrayList<>(tokens);
}
private int scoreStock(StockInfo stock, List<String> tokens)
{
int totalScore = 0;
for (String token : tokens)
{
int tokenScore = 0;
tokenScore += scoreField(stock.getProductName(), token, 120, 90, 60);
tokenScore += scoreField(stock.getSpec(), token, 90, 70, 45);
tokenScore += scoreField(stock.getBrand(), token, 80, 60, 40);
tokenScore += scoreField(stock.getCategory(), token, 70, 50, 35);
tokenScore += scoreField(stock.getAreaName(), token, 65, 45, 30);
tokenScore += scoreField(stock.getUnit(), token, 35, 0, 20);
tokenScore += scoreField(stock.getProductCode(), token, 0, 0, 10);
tokenScore += scoreField(stock.getAreaCode(), token, 0, 0, 10);
if (tokenScore <= 0)
{
return 0;
}
totalScore += tokenScore;
}
return totalScore;
}
private int scoreField(String value, String token, int exactScore, int prefixScore, int containsScore)
{
if (StringUtils.isBlank(value) || StringUtils.isBlank(token))
{
return 0;
}
String lowerValue = value.toLowerCase();
if (exactScore > 0 && lowerValue.equals(token))
{
return exactScore;
}
if (prefixScore > 0 && lowerValue.startsWith(token))
{
return prefixScore;
}
if (containsScore > 0 && lowerValue.contains(token))
{
return containsScore;
}
return 0;
}
private static class ScoredStock
{
private final StockInfo stock;
private final int score;
private ScoredStock(StockInfo stock, int score)
{
this.stock = stock;
this.score = score;
}
} }
@Override @Override

View File

@ -33,8 +33,8 @@
<if test="areaId != null">and si.area_id = #{areaId}</if> <if test="areaId != null">and si.area_id = #{areaId}</if>
<if test="productId != null">and si.product_id = #{productId}</if> <if test="productId != null">and si.product_id = #{productId}</if>
<if test="areaName != null and areaName != ''">and wa.area_name like concat('%', #{areaName}, '%')</if> <if test="areaName != null and areaName != ''">and wa.area_name like concat('%', #{areaName}, '%')</if>
<if test="productName != null and productName != ''"> <if test="productName != null and productName != '' and (params.keywordTokens == null or params.keywordTokens.size() == 0)">
and lower(concat_ws(' ', pi.product_name, pi.brand, pi.category, pi.spec, pi.unit)) and lower(concat_ws(' ', pi.product_name, pi.brand, pi.category, pi.spec, pi.unit, wa.area_name))
like concat('%', lower(#{productName}), '%') like concat('%', lower(#{productName}), '%')
</if> </if>
</where> </where>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.warehouse.mapper.WmsBatchItemMapper">
<resultMap type="WmsBatchItem" id="WmsBatchItemResult">
<id property="id" column="id"/>
<result property="jobId" column="job_id"/>
<result property="rowNo" column="row_no"/>
<result property="actionType" column="action_type"/>
<result property="rawText" column="raw_text"/>
<result property="parsedJson" column="parsed_json"/>
<result property="status" column="status"/>
<result property="errorMsg" column="error_msg"/>
<result property="warningMsg" column="warning_msg"/>
<result property="refId" column="ref_id"/>
<result property="areaName" column="area_name"/>
<result property="productName" column="product_name"/>
<result property="brand" column="brand"/>
<result property="category" column="category"/>
<result property="spec" column="spec"/>
<result property="unit" column="unit"/>
<result property="quantity" column="quantity"/>
<result property="operatorName" column="operator_name"/>
<result property="typeCode" column="type_code"/>
<result property="sourceNo" column="source_no"/>
<result property="remark" column="remark"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectWmsBatchItemVo">
select id, job_id, row_no, action_type, raw_text, parsed_json, status, error_msg, warning_msg, ref_id,
area_name, product_name, brand, category, spec, unit, quantity, operator_name, type_code,
source_no, remark, create_time, update_time
from wms_batch_item
</sql>
<select id="selectWmsBatchItemList" parameterType="WmsBatchItem" resultMap="WmsBatchItemResult">
<include refid="selectWmsBatchItemVo"/>
<where>
<if test="jobId != null">and job_id = #{jobId}</if>
<if test="status != null">and status = #{status}</if>
<if test="actionType != null and actionType != ''">and action_type = #{actionType}</if>
</where>
order by row_no asc, id asc
</select>
<select id="selectWmsBatchItemById" parameterType="Long" resultMap="WmsBatchItemResult">
<include refid="selectWmsBatchItemVo"/> where id = #{id}
</select>
<select id="countByJobIdAndStatus" parameterType="WmsBatchItem" resultType="int">
select count(1) from wms_batch_item where job_id = #{jobId} and status = #{status}
</select>
<insert id="insertWmsBatchItem" parameterType="WmsBatchItem" useGeneratedKeys="true" keyProperty="id">
insert into wms_batch_item(job_id, row_no, action_type, raw_text, parsed_json, status, error_msg, warning_msg,
ref_id, area_name, product_name, brand, category, spec, unit, quantity,
operator_name, type_code, source_no, remark, create_time)
values(#{jobId}, #{rowNo}, #{actionType}, #{rawText}, #{parsedJson}, #{status}, #{errorMsg}, #{warningMsg},
#{refId}, #{areaName}, #{productName}, #{brand}, #{category}, #{spec}, #{unit}, #{quantity},
#{operatorName}, #{typeCode}, #{sourceNo}, #{remark}, sysdate())
</insert>
<update id="updateWmsBatchItem" parameterType="WmsBatchItem">
update wms_batch_item
<set>
row_no = #{rowNo},
action_type = #{actionType},
raw_text = #{rawText},
parsed_json = #{parsedJson},
status = #{status},
error_msg = #{errorMsg},
warning_msg = #{warningMsg},
ref_id = #{refId},
area_name = #{areaName},
product_name = #{productName},
brand = #{brand},
category = #{category},
spec = #{spec},
unit = #{unit},
quantity = #{quantity},
operator_name = #{operatorName},
type_code = #{typeCode},
source_no = #{sourceNo},
remark = #{remark},
update_time = sysdate()
</set>
where id = #{id}
</update>
<delete id="deleteWmsBatchItemById" parameterType="Long">
delete from wms_batch_item where id = #{id}
</delete>
<delete id="deleteWmsBatchItemByJobId" parameterType="Long">
delete from wms_batch_item where job_id = #{value}
</delete>
</mapper>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.warehouse.mapper.WmsBatchJobMapper">
<resultMap type="WmsBatchJob" id="WmsBatchJobResult">
<id property="id" column="id"/>
<result property="jobNo" column="job_no"/>
<result property="sourceType" column="source_type"/>
<result property="bizType" column="biz_type"/>
<result property="fileName" column="file_name"/>
<result property="filePath" column="file_path"/>
<result property="status" column="status"/>
<result property="progress" column="progress"/>
<result property="totalCount" column="total_count"/>
<result property="successCount" column="success_count"/>
<result property="failCount" column="fail_count"/>
<result property="errorMsg" column="error_msg"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectWmsBatchJobVo">
select id, job_no, source_type, biz_type, file_name, file_path, status, progress,
total_count, success_count, fail_count, error_msg, create_by, create_time, update_time
from wms_batch_job
</sql>
<select id="selectWmsBatchJobList" parameterType="WmsBatchJob" resultMap="WmsBatchJobResult">
<include refid="selectWmsBatchJobVo"/>
<where>
<if test="jobNo != null and jobNo != ''">and job_no like concat('%', #{jobNo}, '%')</if>
<if test="sourceType != null and sourceType != ''">and source_type = #{sourceType}</if>
<if test="bizType != null and bizType != ''">and biz_type = #{bizType}</if>
<if test="status != null">and status = #{status}</if>
</where>
order by id desc
</select>
<select id="selectWmsBatchJobById" parameterType="Long" resultMap="WmsBatchJobResult">
<include refid="selectWmsBatchJobVo"/> where id = #{id}
</select>
<insert id="insertWmsBatchJob" parameterType="WmsBatchJob" useGeneratedKeys="true" keyProperty="id">
insert into wms_batch_job(job_no, source_type, biz_type, file_name, file_path, status, progress,
total_count, success_count, fail_count, error_msg, create_by, create_time)
values(#{jobNo}, #{sourceType}, #{bizType}, #{fileName}, #{filePath}, #{status}, #{progress},
#{totalCount}, #{successCount}, #{failCount}, #{errorMsg}, #{createBy}, sysdate())
</insert>
<update id="updateWmsBatchJob" parameterType="WmsBatchJob">
update wms_batch_job
<set>
<if test="sourceType != null">source_type = #{sourceType},</if>
<if test="bizType != null">biz_type = #{bizType},</if>
<if test="fileName != null">file_name = #{fileName},</if>
<if test="filePath != null">file_path = #{filePath},</if>
<if test="status != null">status = #{status},</if>
<if test="progress != null">progress = #{progress},</if>
<if test="totalCount != null">total_count = #{totalCount},</if>
<if test="successCount != null">success_count = #{successCount},</if>
<if test="failCount != null">fail_count = #{failCount},</if>
error_msg = #{errorMsg},
update_time = sysdate()
</set>
where id = #{id}
</update>
</mapper>

View File

@ -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
})
}

View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
export function listBatchJob(query) {
return request({ url: '/warehouse/batch/list', method: 'get', params: query })
}
export function uploadBatchFile(file) {
const data = new FormData()
data.append('file', file)
return request({
url: '/warehouse/batch/upload',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
timeout: 60000
})
}
export function getBatchJob(id) {
return request({ url: '/warehouse/batch/' + id, method: 'get' })
}
export function listBatchItems(jobId) {
return request({ url: '/warehouse/batch/' + jobId + '/items', method: 'get' })
}
export function updateBatchItem(data) {
return request({ url: '/warehouse/batch/item', method: 'put', data })
}
export function updateBatchItemAction(jobId, data) {
return request({ url: '/warehouse/batch/' + jobId + '/items/action', method: 'put', data })
}
export function deleteBatchItem(id) {
return request({ url: '/warehouse/batch/item/' + id, method: 'delete' })
}
export function confirmBatchJob(jobId) {
return request({
url: '/warehouse/batch/' + jobId + '/confirm',
method: 'post',
timeout: 120000,
headers: { repeatSubmit: false }
})
}

View File

@ -67,6 +67,19 @@ export const constantRoutes = [
hidden: true, hidden: true,
meta: { title: '移动库存' } meta: { title: '移动库存' }
}, },
{
path: '/warehouse/batch-import',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/warehouse/batchImport/index'),
name: 'WmsBatchImport',
meta: { title: '批量导入' }
}
]
},
{ {
path: '', path: '',
component: Layout, component: Layout,

View File

@ -0,0 +1,77 @@
function isImageFile(file) {
return file && file.type && file.type.indexOf('image/') === 0
}
function canvasToBlob(canvas, type, quality) {
return new Promise(resolve => {
if (!canvas.toBlob) {
resolve(null)
return
}
canvas.toBlob(blob => resolve(blob), type, quality)
})
}
function loadImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = event => {
const image = new Image()
image.onload = () => resolve(image)
image.onerror = reject
image.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}
export async function compressImageForUpload(file, options = {}) {
const maxSize = options.maxSize || 3 * 1024 * 1024
const maxWidth = options.maxWidth || 1600
const maxHeight = options.maxHeight || 1600
const outputType = options.outputType || 'image/jpeg'
const minQuality = options.minQuality || 0.55
if (!isImageFile(file) || typeof FileReader === 'undefined' || typeof document === 'undefined') {
return file
}
try {
const image = await loadImage(file)
let scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1)
let quality = options.quality || 0.82
let bestBlob = null
for (let i = 0; i < 7; i++) {
const width = Math.max(1, Math.round(image.width * scale))
const height = Math.max(1, Math.round(image.height * scale))
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
context.fillStyle = '#ffffff'
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height)
const blob = await canvasToBlob(canvas, outputType, quality)
if (!blob) {
return file
}
bestBlob = blob
if (blob.size <= maxSize) {
break
}
scale = scale * 0.82
quality = Math.max(minQuality, quality - 0.08)
}
if (!bestBlob || bestBlob.size >= file.size) {
return file
}
const fileName = file.name ? file.name.replace(/\.[^.]+$/, '.jpg') : 'upload.jpg'
return new File([bestBlob], fileName, { type: outputType, lastModified: Date.now() })
} catch (error) {
return file
}
}

View File

@ -5,7 +5,77 @@
<h1>库存管理系统</h1> <h1>库存管理系统</h1>
<p>面向库区产品入库出库和盘库的日常库存作业台</p> <p>面向库区产品入库出库和盘库的日常库存作业台</p>
</div> </div>
<el-button type="primary" icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button> <div class="hero-actions">
<el-button type="primary" icon="el-icon-upload2" @click="$router.push('/warehouse/batch-import')">批量导入</el-button>
<el-button type="primary" plain icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
</div>
</section>
<section class="agent-panel">
<div class="agent-title">
<div>
<h2>智能库存助手</h2>
<p>输入一句话系统先生成操作草稿确认后再真正执行</p>
</div>
<el-tag size="small" type="info">第一阶段</el-tag>
</div>
<div class="agent-input">
<el-input
v-model="agent.text"
type="textarea"
:rows="2"
resize="none"
maxlength="200"
show-word-limit
placeholder="例如:销售出库 10 个某产品,库区 A出库人张三"
@keyup.enter.native="analyzeAgentText"
/>
<el-button icon="el-icon-delete" :disabled="!agent.text && !agent.plan" @click="clearAgent">清空</el-button>
<el-button type="primary" icon="el-icon-magic-stick" :loading="agent.loading" @click="analyzeAgentText">分析</el-button>
</div>
<div v-if="agent.plan" class="agent-result">
<div class="agent-plan">
<div>
<el-tag size="small" :type="agent.plan.executable ? 'success' : 'warning'">{{ actionLabel(agent.plan.action) }}</el-tag>
<strong>{{ agent.plan.summary || agent.plan.message || '已生成操作草稿' }}</strong>
</div>
<el-button
v-if="agent.plan.needConfirm"
type="danger"
size="small"
icon="el-icon-check"
:disabled="!agent.plan.executable"
:loading="agent.executing"
@click="executeAgentPlan"
>确认执行</el-button>
</div>
<el-descriptions v-if="planItems.length" :column="3" size="small" border>
<el-descriptions-item v-for="item in planItems" :key="item.label" :label="item.label">{{ item.value }}</el-descriptions-item>
</el-descriptions>
<el-alert v-if="agent.plan.message" :title="agent.plan.message" type="info" show-icon :closable="false" />
<div v-if="agent.plan.warnings && agent.plan.warnings.length" class="agent-warnings">
<el-tag v-for="item in agent.plan.warnings" :key="item" type="warning" size="small">{{ item }}</el-tag>
</div>
<div v-if="agent.plan.candidates && agent.plan.candidates.length" class="agent-candidates">
<span v-for="item in agent.plan.candidates" :key="item.type + item.id">{{ item.name }} {{ item.detail || '' }}</span>
</div>
<el-table v-if="agent.plan.results && agent.plan.results.length" :data="agent.plan.results" size="mini" class="agent-table">
<el-table-column type="index" label="#" width="54" />
<el-table-column v-if="canSelectAgentResult(agent.plan)" label="选择" width="86">
<template slot-scope="scope">
<el-button type="primary" size="mini" plain @click="selectAgentResult(scope.row)">选择</el-button>
</template>
</el-table-column>
<el-table-column label="库区" prop="areaName" width="120" />
<el-table-column label="产品" min-width="220">
<template slot-scope="scope">
<div class="product-name">{{ scope.row.productName || '-' }}</div>
<div class="product-meta">{{ productMeta(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="库存" prop="stockNum" align="right" width="110" />
</el-table>
</div>
</section> </section>
<section class="summary-grid"> <section class="summary-grid">
@ -85,6 +155,7 @@ import { listStock } from '@/api/warehouse/stock'
import { listInRecord } from '@/api/warehouse/inRecord' import { listInRecord } from '@/api/warehouse/inRecord'
import { listOutRecord } from '@/api/warehouse/outRecord' import { listOutRecord } from '@/api/warehouse/outRecord'
import { listStockCheck } from '@/api/warehouse/stockCheck' import { listStockCheck } from '@/api/warehouse/stockCheck'
import { analyzeAgent, executeAgent } from '@/api/warehouse/agent'
export default { export default {
name: 'Index', name: 'Index',
@ -96,13 +167,142 @@ export default {
pendingCheck: 0, pendingCheck: 0,
outTotal: 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() { created() {
this.loadDashboard() this.loadDashboard()
}, },
methods: { 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
},
canSelectAgentResult(plan) {
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
},
selectAgentResult(item) {
if (!this.agent.plan || !item) {
return
}
const plan = this.completePlanWithResult(this.agent.plan, item)
this.agent.plan = plan
this.agent.contextResults = [item]
this.$modal.msgSuccess('已选择:' + (item.productName || '库存项'))
},
completePlanWithResult(source, item) {
const plan = Object.assign({}, source, {
stockId: item.id,
areaId: item.areaId,
productId: item.productId,
areaName: item.areaName,
productName: item.productName,
brand: item.brand,
category: item.category,
spec: item.spec,
unit: item.unit,
currentStock: item.stockNum,
candidates: [],
results: [],
needConfirm: true,
message: '已选择产品,请确认后执行'
})
const quantity = this.toNumber(plan.quantity)
const stock = this.toNumber(item.stockNum)
if (plan.action === 'create_out_record') {
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock - quantity : undefined
if (plan.executable && stock < quantity) {
plan.executable = false
plan.message = '库存不足,当前库存:' + item.stockNum
}
} else if (plan.action === 'create_in_record') {
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock + quantity : undefined
} else if (plan.action === 'create_stock_check') {
plan.executable = !!(plan.areaId && plan.productId && quantity >= 0)
plan.afterStock = quantity >= 0 ? quantity : undefined
}
if (!plan.executable && plan.message === '已选择产品,请确认后执行') {
plan.message = '已选择产品,还缺少库区或数量'
}
return plan
},
toNumber(value) {
const numberValue = Number(value)
return Number.isFinite(numberValue) ? numberValue : 0
},
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() { loadDashboard() {
listStock({ pageNum: 1, pageSize: 8 }).then(res => { listStock({ pageNum: 1, pageSize: 8 }).then(res => {
this.stockList = res.rows || [] this.stockList = res.rows || []
@ -140,6 +340,12 @@ export default {
border: 1px solid #e6ebf2; border: 1px solid #e6ebf2;
border-radius: 8px; border-radius: 8px;
} }
.hero-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.hero h1 { .hero h1 {
margin: 0; margin: 0;
font-size: 24px; font-size: 24px;
@ -150,6 +356,76 @@ export default {
margin: 6px 0 0; margin: 6px 0 0;
color: #7a8794; 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 { .summary-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@ -222,6 +498,16 @@ export default {
align-items: flex-start; align-items: flex-start;
flex-direction: column; 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 { .summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,415 @@
<template>
<div class="app-container batch-page">
<el-card shadow="never" class="batch-card">
<div slot="header" class="card-head">
<div>
<span class="title">智能批量导入</span>
<span class="subtitle">支持 Excel 和图片解析后先确认明细再写入库存业务表</span>
</div>
<el-button icon="el-icon-refresh" size="mini" @click="loadJobs">刷新任务</el-button>
</div>
<el-upload
class="upload-box"
drag
action="#"
accept=".xls,.xlsx,.jpg,.jpeg,.png,.bmp"
:show-file-list="false"
:http-request="handleUpload"
:disabled="uploading"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">拖入 Excel 或图片 <em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">Excel 支持 xls/xlsx图片支持 jpg/png/bmp图片会调用大模型识别耗时可能更久</div>
</el-upload>
<div v-if="currentJob" class="job-panel">
<div class="job-main">
<div>
<div class="job-no">{{ currentJob.jobNo }}</div>
<div class="job-meta">{{ currentJob.fileName }} {{ statusLabel(currentJob.status) }}</div>
</div>
<el-button
type="danger"
icon="el-icon-check"
:loading="confirming"
:disabled="currentJob.status !== 2 || currentJob.failCount > 0 || !items.length"
@click="handleConfirm"
>确认执行</el-button>
</div>
<el-progress :percentage="currentJob.progress || 0" :status="progressStatus(currentJob)" />
<div class="job-stats">
<el-tag size="small">总数 {{ currentJob.totalCount || 0 }}</el-tag>
<el-tag size="small" type="success">可执行 {{ currentJob.successCount || 0 }}</el-tag>
<el-tag size="small" type="danger">异常 {{ currentJob.failCount || 0 }}</el-tag>
<el-alert v-if="currentJob.errorMsg" :title="currentJob.errorMsg" type="warning" show-icon :closable="false" />
</div>
</div>
</el-card>
<el-row :gutter="16">
<el-col :xs="24" :lg="7">
<el-card shadow="never" class="batch-card">
<div slot="header" class="card-head">
<span class="title">最近任务</span>
</div>
<el-table v-loading="jobLoading" :data="jobList" size="mini" height="420" @row-click="selectJob">
<el-table-column label="任务号" prop="jobNo" min-width="150" show-overflow-tooltip />
<el-table-column label="来源" width="72">
<template slot-scope="scope">{{ sourceLabel(scope.row.sourceType) }}</template>
</el-table-column>
<el-table-column label="状态" width="86">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="进度" prop="progress" width="70" align="right" />
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :lg="17">
<el-card shadow="never" class="batch-card">
<div slot="header" class="card-head">
<span class="title">待确认明细</span>
<span class="subtitle">异常明细需要修改或删除后才能确认执行</span>
</div>
<div class="batch-actions">
<el-select v-model="batchActionType" size="small" placeholder="批量设置操作类型" clearable>
<el-option label="新增库区" value="create_area" />
<el-option label="新增产品" value="create_product" />
<el-option label="入库登记" value="create_in_record" />
<el-option label="出库" value="create_out_record" />
<el-option label="盘库登记" value="create_stock_check" />
</el-select>
<el-button size="small" type="primary" plain :loading="batchSaving" :disabled="!canEditItems || !batchActionType || !items.length" @click="applyBatchAction(false)">应用到全部</el-button>
<el-button size="small" type="primary" plain :loading="batchSaving" :disabled="!canEditItems || !batchActionType || !selectedItems.length" @click="applyBatchAction(true)">应用到已选 {{ selectedItems.length }}</el-button>
</div>
<el-table v-loading="itemLoading" :data="items" size="mini" height="520" @selection-change="handleItemSelectionChange">
<el-table-column type="selection" width="45" />
<el-table-column label="行" prop="rowNo" width="58" />
<el-table-column label="操作" width="110">
<template slot-scope="scope">{{ actionLabel(scope.row.actionType) }}</template>
</el-table-column>
<el-table-column label="状态" width="92">
<template slot-scope="scope">
<el-tag :type="itemStatusType(scope.row.status)" size="mini">{{ itemStatusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="库区" prop="areaName" width="110" show-overflow-tooltip />
<el-table-column label="产品" min-width="190" show-overflow-tooltip>
<template slot-scope="scope">
<div>{{ scope.row.productName || '-' }}</div>
<div class="meta">{{ productMeta(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="数量" prop="quantity" width="90" align="right" />
<el-table-column label="人员" prop="operatorName" width="100" show-overflow-tooltip />
<el-table-column label="提示/错误" min-width="220" show-overflow-tooltip>
<template slot-scope="scope">
<span class="error-text" v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</span>
<span v-else>{{ scope.row.warningMsg || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openEdit(scope.row)">修改</el-button>
<el-button type="text" size="mini" class="danger-link" @click="removeItem(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-dialog title="修改明细" :visible.sync="editOpen" width="620px" append-to-body>
<el-form ref="editForm" :model="editForm" label-width="90px">
<el-form-item label="操作类型">
<el-select v-model="editForm.actionType" placeholder="请选择操作类型" style="width: 100%">
<el-option label="新增库区" value="create_area" />
<el-option label="新增产品" value="create_product" />
<el-option label="入库登记" value="create_in_record" />
<el-option label="出库" value="create_out_record" />
<el-option label="盘库登记" value="create_stock_check" />
</el-select>
</el-form-item>
<el-form-item label="库区"><el-input v-model="editForm.areaName" /></el-form-item>
<el-form-item label="产品名称"><el-input v-model="editForm.productName" /></el-form-item>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="品牌"><el-input v-model="editForm.brand" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="类别"><el-input v-model="editForm.category" /></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="规格"><el-input v-model="editForm.spec" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="单位"><el-input v-model="editForm.unit" /></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="数量"><el-input-number v-model="editForm.quantity" :precision="3" :min="0" style="width: 100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="类型编码"><el-input-number v-model="editForm.typeCode" :min="1" :max="4" style="width: 100%" /></el-form-item></el-col>
</el-row>
<el-form-item label="操作人"><el-input v-model="editForm.operatorName" /></el-form-item>
<el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" /></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="savingItem" @click="saveItem"> </el-button>
<el-button @click="editOpen = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listBatchJob, uploadBatchFile, getBatchJob, listBatchItems, updateBatchItem, updateBatchItemAction, deleteBatchItem, confirmBatchJob } from '@/api/warehouse/batchImport'
export default {
name: 'WmsBatchImport',
data() {
return {
uploading: false,
confirming: false,
jobLoading: false,
itemLoading: false,
savingItem: false,
batchSaving: false,
batchActionType: undefined,
jobList: [],
currentJob: null,
items: [],
selectedItems: [],
pollTimer: null,
editOpen: false,
editForm: {}
}
},
created() {
this.loadJobs()
},
beforeDestroy() {
this.stopPolling()
},
methods: {
handleUpload(option) {
this.uploading = true
uploadBatchFile(option.file).then(res => {
this.currentJob = res.data
this.items = []
this.$modal.msgSuccess('上传成功,正在后台解析,请留意进度')
this.startPolling(res.data.id)
this.loadJobs()
}).finally(() => {
this.uploading = false
})
},
loadJobs() {
this.jobLoading = true
listBatchJob({ pageNum: 1, pageSize: 20 }).then(res => {
this.jobList = res.rows || []
}).finally(() => {
this.jobLoading = false
})
},
selectJob(row) {
this.currentJob = row
this.loadItems(row.id)
if ([0, 1, 3].includes(row.status)) {
this.startPolling(row.id)
}
},
startPolling(jobId) {
this.stopPolling()
this.pollTimer = setInterval(() => {
this.refreshJob(jobId)
}, 1500)
this.refreshJob(jobId)
},
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
},
refreshJob(jobId) {
getBatchJob(jobId).then(res => {
this.currentJob = res.data
if (this.currentJob && [2, 4, 5, 6].includes(this.currentJob.status)) {
this.stopPolling()
this.loadItems(jobId)
this.loadJobs()
}
})
},
loadItems(jobId) {
this.itemLoading = true
listBatchItems(jobId).then(res => {
this.items = res.data || []
this.selectedItems = []
}).finally(() => {
this.itemLoading = false
})
},
handleItemSelectionChange(selection) {
this.selectedItems = selection
},
openEdit(row) {
this.editForm = Object.assign({}, row)
this.editOpen = true
},
saveItem() {
this.savingItem = true
updateBatchItem(this.editForm).then(res => {
this.$modal.msgSuccess('明细已更新')
this.editOpen = false
this.loadItems(this.currentJob.id)
this.refreshJob(this.currentJob.id)
}).finally(() => {
this.savingItem = false
})
},
removeItem(row) {
this.$modal.confirm('确认删除第 ' + row.rowNo + ' 行明细?').then(() => deleteBatchItem(row.id)).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadItems(this.currentJob.id)
this.refreshJob(this.currentJob.id)
})
},
applyBatchAction(onlySelected) {
const rows = onlySelected ? this.selectedItems : this.items
if (!rows.length || !this.batchActionType) {
return
}
const label = this.actionLabel(this.batchActionType)
this.$modal.confirm('确认将' + (onlySelected ? '已选' : '全部') + '明细的操作类型设置为“' + label + '”?').then(() => {
this.batchSaving = true
const itemIds = onlySelected ? rows.map(item => item.id) : undefined
return updateBatchItemAction(this.currentJob.id, { actionType: this.batchActionType, itemIds })
}).then(() => {
this.$modal.msgSuccess('批量设置完成')
this.loadItems(this.currentJob.id)
this.refreshJob(this.currentJob.id)
}).finally(() => {
this.batchSaving = false
})
},
handleConfirm() {
this.$modal.confirm('确认执行当前批量任务?执行后会真正写入库存业务数据。').then(() => {
this.confirming = true
return confirmBatchJob(this.currentJob.id)
}).then(res => {
this.currentJob = res.data
this.$modal.msgSuccess('执行完成')
this.loadItems(this.currentJob.id)
this.loadJobs()
}).finally(() => {
this.confirming = false
})
},
statusLabel(status) {
return { 0: '已上传', 1: '解析中', 2: '待确认', 3: '执行中', 4: '完成', 5: '失败', 6: '已取消' }[status] || '-'
},
statusType(status) {
return { 2: 'warning', 4: 'success', 5: 'danger', 6: 'info' }[status] || 'info'
},
sourceLabel(sourceType) {
return { excel: 'Excel', image: '图片', file: '文件' }[sourceType] || sourceType || '-'
},
itemStatusLabel(status) {
return { 1: '可执行', 2: '异常', 3: '已执行', 4: '失败' }[status] || '-'
},
itemStatusType(status) {
return { 1: 'success', 2: 'danger', 3: 'success', 4: 'danger' }[status] || 'info'
},
progressStatus(job) {
if (!job) return undefined
if (job.status === 4) return 'success'
if (job.status === 5) return 'exception'
return undefined
},
actionLabel(action) {
return {
create_area: '新增库区',
create_product: '新增产品',
create_in_record: '入库登记',
create_out_record: '出库',
create_stock_check: '盘库登记',
unknown: '未识别'
}[action] || action || '-'
},
productMeta(row) {
return [row.brand, row.category, row.spec, row.unit].filter(Boolean).join(' / ')
}
},
computed: {
canEditItems() {
return this.currentJob && ![3, 4].includes(this.currentJob.status)
}
}
}
</script>
<style scoped lang="scss">
.batch-page {
background: #f5f7fb;
}
.batch-card {
margin-bottom: 16px;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.title {
font-weight: 700;
color: #1f2d3d;
}
.subtitle {
margin-left: 10px;
color: #7a8794;
font-size: 12px;
}
.upload-box {
max-width: 620px;
}
.job-panel {
margin-top: 16px;
padding: 14px;
border: 1px solid #e6ebf2;
border-radius: 6px;
background: #fbfcfe;
}
.job-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.job-no {
font-weight: 700;
color: #1f2d3d;
}
.job-meta,
.meta {
color: #909399;
font-size: 12px;
}
.job-stats {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.batch-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.error-text,
.danger-link {
color: #f56c6c;
}
</style>

View File

@ -198,7 +198,7 @@ export default {
}, },
cancel() { this.open = false; this.reset() }, cancel() { this.open = false; this.reset() },
reset() { reset() {
this.form = { id: undefined, inNo: undefined, areaId: undefined, productId: undefined, inOperator: undefined, inType: 1, inNum: undefined, remark: undefined } this.form = { id: undefined, inNo: undefined, areaId: undefined, productId: undefined, inOperator: undefined, inType: 2, inNum: undefined, remark: undefined }
this.resetForm('form') this.resetForm('form')
}, },
handleQuery() { this.queryParams.pageNum = 1; this.getList() }, handleQuery() { this.queryParams.pageNum = 1; this.getList() },

View File

@ -6,8 +6,8 @@
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" /> <el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="产品信息" prop="productName"> <el-form-item label="关键词" prop="productName">
<el-input v-model="queryParams.productName" placeholder="名称/品牌/类别/规格/单位" clearable @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.productName" placeholder="名称/规格/品牌/类别/单位/库区,如:电机 20" clearable @keyup.enter.native="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>

49
sql/wms_batch_import.sql Normal file
View File

@ -0,0 +1,49 @@
-- 批量导入任务表。执行一次即可。
create table if not exists wms_batch_job (
id bigint not null auto_increment comment '主键',
job_no varchar(64) not null comment '任务号',
source_type varchar(20) not null comment '来源类型 excel/image/file',
biz_type varchar(30) default 'mixed' comment '业务类型',
file_name varchar(255) default null comment '原始文件名',
file_path varchar(500) default null comment '服务器文件路径',
status tinyint not null default 0 comment '状态 0上传完成 1解析中 2待确认 3执行中 4完成 5失败 6取消',
progress int not null default 0 comment '进度',
total_count int not null default 0 comment '总条数',
success_count int not null default 0 comment '成功条数',
fail_count int not null default 0 comment '失败条数',
error_msg varchar(1000) default null comment '错误信息',
create_by varchar(64) default null comment '创建人',
create_time datetime default null comment '创建时间',
update_time datetime default null comment '更新时间',
primary key (id),
unique key uk_wms_batch_job_no (job_no)
) engine=innodb default charset=utf8mb4 comment='WMS批量导入任务';
create table if not exists wms_batch_item (
id bigint not null auto_increment comment '主键',
job_id bigint not null comment '任务ID',
row_no int default null comment 'Excel行号',
action_type varchar(40) not null comment '操作类型',
raw_text text comment '原始内容',
parsed_json text comment '解析JSON',
status tinyint not null default 1 comment '状态 1校验通过 2校验失败 3已执行 4执行失败',
error_msg varchar(1000) default null comment '错误信息',
warning_msg varchar(1000) default null comment '提示信息',
ref_id bigint default null comment '执行后业务ID',
area_name varchar(100) default null comment '库区名称',
product_name varchar(200) default null comment '产品名称',
brand varchar(100) default null comment '品牌',
category varchar(100) default null comment '类别',
spec varchar(200) default null comment '规格',
unit varchar(50) default null comment '单位',
quantity decimal(16,3) default null comment '数量',
operator_name varchar(100) default null comment '操作人',
type_code tinyint default null comment '入库/出库类型',
source_no varchar(100) default null comment '来源单据号',
remark varchar(500) default null comment '备注',
create_time datetime default null comment '创建时间',
update_time datetime default null comment '更新时间',
primary key (id),
key idx_wms_batch_item_job (job_id),
key idx_wms_batch_item_status (status)
) engine=innodb default charset=utf8mb4 comment='WMS批量导入明细';