From 2ea0df3e1bf1e8e412a54f54b7689925e1e7515c Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Tue, 15 Sep 2026 15:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(warehouse):=20=E5=8D=87=E7=BA=A7=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=90=86=E6=9C=8D=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成大模型配置支持,包括API密钥、超时设置和输出令牌限制 - 优化意图解析算法,提升数量提取和引用识别准确性 - 增加入库单关联采购请求和到货单功能 - 完善入库确认前的库区和操作员验证机制 - 重构HTTP客户端配置,统一连接超时和协议版本设置 - 优化前端对话框组件,改进错误处理和结果显示 - 添加移动端上下文引用功能,增强对话连续性 - 扩展入库记录表格显示来源单号和待分配状态 - 实现Excel导入组件错误处理和更新选项控制 --- .../warehouse/PurchaseRequestController.java | 239 +++++ .../warehouse/PurchaseTemplateExcelUtil.java | 264 +++++ .../src/main/resources/application.yml | 7 + .../framework/web/service/TokenService.java | 36 +- .../com/ruoyi/warehouse/domain/InRecord.java | 13 + .../warehouse/domain/PurchaseArrival.java | 96 ++ .../PurchaseArrivalAcceptanceRequest.java | 15 + .../domain/PurchaseArrivalRevokeRequest.java | 19 + .../ruoyi/warehouse/domain/PurchaseBatch.java | 28 + .../domain/PurchaseBatchAssignRequest.java | 12 + .../warehouse/domain/PurchaseRequest.java | 144 +++ .../domain/PurchaseStatusUpdateRequest.java | 15 + .../warehouse/mapper/InRecordMapper.java | 4 + .../mapper/PurchaseArrivalMapper.java | 19 + .../warehouse/mapper/PurchaseBatchMapper.java | 22 + .../mapper/PurchaseRequestMapper.java | 18 + .../service/IPurchaseArrivalService.java | 15 + .../service/IPurchaseBatchService.java | 16 + .../service/IPurchaseRequestService.java | 17 + .../service/IPurchaseSmartImportService.java | 9 + .../service/impl/AgentServiceImpl.java | 315 +++++- .../service/impl/InRecordServiceImpl.java | 23 + .../impl/PurchaseArrivalServiceImpl.java | 274 +++++ .../impl/PurchaseBatchServiceImpl.java | 141 +++ .../impl/PurchaseRequestServiceImpl.java | 411 ++++++++ .../impl/PurchaseSmartImportServiceImpl.java | 756 ++++++++++++++ .../impl/WmsBatchImportServiceImpl.java | 65 +- .../mapper/warehouse/InRecordMapper.xml | 25 +- .../warehouse/PurchaseArrivalMapper.xml | 104 ++ .../mapper/warehouse/PurchaseBatchMapper.xml | 107 ++ .../warehouse/PurchaseRequestMapper.xml | 148 +++ ruoyi-ui/src/api/warehouse/purchase.js | 87 ++ .../components/ExcelImportDialog/index.vue | 25 +- ruoyi-ui/src/views/index.vue | 13 +- ruoyi-ui/src/views/mobile/index.vue | 10 +- .../src/views/warehouse/inRecord/index.vue | 23 +- .../src/views/warehouse/purchase/index.vue | 945 ++++++++++++++++++ sql/wms_inventory.sql | 7 +- sql/wms_purchase_request.sql | 279 ++++++ 39 files changed, 4672 insertions(+), 94 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseRequestController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseTemplateExcelUtil.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrival.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalAcceptanceRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalRevokeRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatch.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatchAssignRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseStatusUpdateRequest.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseArrivalMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseBatchMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseRequestMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseArrivalService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseBatchService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseRequestService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseSmartImportService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseArrivalServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseBatchServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseRequestServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseSmartImportServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/warehouse/PurchaseArrivalMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/warehouse/PurchaseBatchMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/warehouse/PurchaseRequestMapper.xml create mode 100644 ruoyi-ui/src/api/warehouse/purchase.js create mode 100644 ruoyi-ui/src/views/warehouse/purchase/index.vue create mode 100644 sql/wms_purchase_request.sql diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseRequestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseRequestController.java new file mode 100644 index 0000000..45b8e7e --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseRequestController.java @@ -0,0 +1,239 @@ +package com.ruoyi.web.controller.warehouse; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +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.common.utils.poi.ExcelUtil; +import com.ruoyi.warehouse.domain.PurchaseBatch; +import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest; +import com.ruoyi.warehouse.domain.PurchaseArrival; +import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest; +import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest; +import com.ruoyi.warehouse.domain.PurchaseRequest; +import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest; +import com.ruoyi.warehouse.service.IPurchaseArrivalService; +import com.ruoyi.warehouse.service.IPurchaseBatchService; +import com.ruoyi.warehouse.service.IProductInfoService; +import com.ruoyi.warehouse.service.IPurchaseRequestService; +import com.ruoyi.warehouse.service.IPurchaseSmartImportService; + +@RestController +@RequestMapping("/warehouse/purchase") +public class PurchaseRequestController extends BaseController +{ + @Autowired + private IPurchaseRequestService purchaseRequestService; + + @Autowired + private IPurchaseBatchService purchaseBatchService; + + @Autowired + private IPurchaseArrivalService purchaseArrivalService; + + @Autowired + private IProductInfoService productInfoService; + + @Autowired + private IPurchaseSmartImportService purchaseSmartImportService; + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')") + @GetMapping("/list") + public TableDataInfo list(PurchaseRequest purchaseRequest) + { + startPage(); + return getDataTable(purchaseRequestService.selectPurchaseRequestList(purchaseRequest)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:export,warehouse:purchase:manage')") + @Log(title = "采购申请", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PurchaseRequest purchaseRequest) throws Exception + { + List list = purchaseRequestService.selectPurchaseRequestList(purchaseRequest); + PurchaseTemplateExcelUtil.writeExport(response, list, productInfoService.selectProductInfoAll()); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:import,warehouse:purchase:manage')") + @Log(title = "采购申请", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file) throws Exception + { + ExcelUtil util = new ExcelUtil(PurchaseRequest.class); + List requestList = util.importExcel(file.getInputStream()); + return success(purchaseRequestService.importPurchaseRequest(requestList, getUsername())); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')") + @Log(title = "采购申请智能导入", businessType = BusinessType.IMPORT) + @PostMapping("/smart-import") + public AjaxResult smartImport(MultipartFile file) throws Exception + { + return success(purchaseSmartImportService.analyze(file)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:import,warehouse:purchase:manage')") + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) throws Exception + { + PurchaseTemplateExcelUtil.write(response, productInfoService.selectProductInfoAll()); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:query,warehouse:purchase:manage')") + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable Long id) + { + return success(purchaseRequestService.selectPurchaseRequestById(id)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')") + @Log(title = "采购申请", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody PurchaseRequest purchaseRequest) + { + return toAjax(purchaseRequestService.insertPurchaseRequest(purchaseRequest)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')") + @Log(title = "采购申请表格录入", businessType = BusinessType.INSERT) + @PostMapping("/table") + public AjaxResult tableAdd(@RequestBody List requestList) + { + return success(purchaseRequestService.batchInsertPurchaseRequest(requestList)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')") + @Log(title = "采购申请", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody PurchaseRequest purchaseRequest) + { + return toAjax(purchaseRequestService.updatePurchaseRequest(purchaseRequest)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')") + @Log(title = "采购状态", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/status") + public AjaxResult updateStatus(@PathVariable Long id, + @Validated @RequestBody PurchaseStatusUpdateRequest request) + { + return toAjax(purchaseRequestService.updatePurchaseStatus(id, request)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')") + @GetMapping("/arrival/{requestId}") + public AjaxResult arrivalList(@PathVariable Long requestId) + { + return success(purchaseArrivalService.selectPurchaseArrivalList(requestId)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')") + @Log(title = "采购到货", businessType = BusinessType.INSERT) + @PostMapping("/arrival") + public AjaxResult addArrival(@Validated @RequestBody PurchaseArrival arrival) + { + return toAjax(purchaseArrivalService.insertPurchaseArrival(arrival)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')") + @Log(title = "采购到货验收", businessType = BusinessType.UPDATE) + @PutMapping("/arrival/{id}/accept") + public AjaxResult acceptArrival(@PathVariable Long id, + @Validated @RequestBody PurchaseArrivalAcceptanceRequest request) + { + return toAjax(purchaseArrivalService.acceptPurchaseArrival(id, request)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')") + @Log(title = "撤销采购到货验收", businessType = BusinessType.UPDATE) + @PutMapping("/arrival/{id}/revoke") + public AjaxResult revokeArrival(@PathVariable Long id, + @Validated @RequestBody PurchaseArrivalRevokeRequest request) + { + return toAjax(purchaseArrivalService.revokePurchaseArrivalAcceptance(id, request)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:remove,warehouse:purchase:manage')") + @Log(title = "采购到货", businessType = BusinessType.DELETE) + @DeleteMapping("/arrival/{id}") + public AjaxResult removeArrival(@PathVariable Long id) + { + return toAjax(purchaseArrivalService.deletePurchaseArrival(id)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @GetMapping("/batch/list") + public TableDataInfo batchList(PurchaseBatch batch) + { + startPage(); + return getDataTable(purchaseBatchService.selectPurchaseBatchList(batch)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @GetMapping("/batch/options") + public AjaxResult batchOptions(String keyword) + { + return success(purchaseBatchService.selectPurchaseBatchOptions(keyword)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')") + @GetMapping("/batch/filter-options") + public AjaxResult batchFilterOptions(String keyword) + { + return success(purchaseBatchService.selectPurchaseBatchFilterOptions(keyword)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @Log(title = "采购批次", businessType = BusinessType.INSERT) + @PostMapping("/batch") + public AjaxResult addBatch(@RequestBody PurchaseBatch batch) + { + return toAjax(purchaseBatchService.insertPurchaseBatch(batch)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @Log(title = "采购批次", businessType = BusinessType.UPDATE) + @PutMapping("/batch") + public AjaxResult editBatch(@RequestBody PurchaseBatch batch) + { + return toAjax(purchaseBatchService.updatePurchaseBatch(batch)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @Log(title = "采购批次分配", businessType = BusinessType.UPDATE) + @PutMapping("/batch/assign") + public AjaxResult assignBatch(@RequestBody PurchaseBatchAssignRequest request) + { + return toAjax(purchaseBatchService.assignPurchaseBatch(request)); + } + + @PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')") + @Log(title = "采购批次", businessType = BusinessType.DELETE) + @DeleteMapping("/batch/{id}") + public AjaxResult removeBatch(@PathVariable Long id) + { + return toAjax(purchaseBatchService.deletePurchaseBatchById(id)); + } + + @PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:remove,warehouse:purchase:manage')") + @Log(title = "采购申请", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(purchaseRequestService.deletePurchaseRequestByIds(ids)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseTemplateExcelUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseTemplateExcelUtil.java new file mode 100644 index 0000000..ab09efb --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/warehouse/PurchaseTemplateExcelUtil.java @@ -0,0 +1,264 @@ +package com.ruoyi.web.controller.warehouse; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DataValidation.ErrorStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.SheetVisibility; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.warehouse.domain.ProductInfo; +import com.ruoyi.warehouse.domain.PurchaseRequest; + +public final class PurchaseTemplateExcelUtil +{ + private static final String[] HEADERS = { + "关联物料编码", "物料名称", "品牌", "类别", "参数规格", "采购链接", + "需求数量", "单位", "期望到货日期", "产品备注", "申请备注" + }; + + private static final String[] EXPORT_HEADERS = { + "关联物料编码", "物料名称", "品牌", "类别", "参数规格", "采购链接", + "需求数量", "单位", "期望到货日期", "申请人", "采购状态", "采购批次", "进度说明", + "产品备注", "申请备注", "已验收数量", "待验收数量" + }; + + private PurchaseTemplateExcelUtil() + { + } + + public static void write(HttpServletResponse response, List products) throws IOException + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + FileUtils.setAttachmentResponseHeader(response, "采购申请导入模板.xlsx"); + + try (XSSFWorkbook workbook = createTemplateWorkbook(products)) + { + workbook.write(response.getOutputStream()); + } + } + + static XSSFWorkbook createTemplateWorkbook(List products) + { + XSSFWorkbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("采购申请数据"); + Sheet options = workbook.createSheet("系统下拉数据"); + CellStyle headerStyle = createHeaderStyle(workbook); + Row header = sheet.createRow(0); + for (int index = 0; index < HEADERS.length; index++) + { + Cell cell = header.createCell(index); + cell.setCellValue(HEADERS[index]); + cell.setCellStyle(headerStyle); + sheet.setColumnWidth(index, columnWidth(index)); + } + sheet.createFreezePane(0, 1); + + addOptionColumn(workbook, sheet, options, 0, "物料编码", values(products, "code")); + addOptionColumn(workbook, sheet, options, 1, "物料名称", values(products, "name")); + addOptionColumn(workbook, sheet, options, 2, "品牌", values(products, "brand")); + addOptionColumn(workbook, sheet, options, 3, "类别", values(products, "category")); + addOptionColumn(workbook, sheet, options, 4, "参数规格", values(products, "spec")); + addOptionColumn(workbook, sheet, options, 7, "单位", values(products, "unit")); + workbook.setSheetVisibility(workbook.getSheetIndex(options), SheetVisibility.HIDDEN); + return workbook; + } + + public static void writeExport(HttpServletResponse response, List requests, + List products) throws IOException + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + FileUtils.setAttachmentResponseHeader(response, "采购申请数据.xlsx"); + + try (XSSFWorkbook workbook = new XSSFWorkbook()) + { + Sheet sheet = workbook.createSheet("采购申请数据"); + Sheet options = workbook.createSheet("系统下拉数据"); + CellStyle headerStyle = createHeaderStyle(workbook); + Row header = sheet.createRow(0); + for (int index = 0; index < EXPORT_HEADERS.length; index++) + { + Cell cell = header.createCell(index); + cell.setCellValue(EXPORT_HEADERS[index]); + cell.setCellStyle(headerStyle); + sheet.setColumnWidth(index, exportColumnWidth(index)); + } + sheet.createFreezePane(0, 1); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + for (int rowIndex = 0; rowIndex < requests.size(); rowIndex++) + { + PurchaseRequest request = requests.get(rowIndex); + Row row = sheet.createRow(rowIndex + 1); + Object[] values = { + request.getProductCode(), request.getMaterialName(), request.getMaterialBrand(), + request.getCategory(), request.getMaterialParams(), + request.getPurchaseLink(), request.getQuantity(), request.getUnit(), + request.getExpectedDate() == null ? null : dateFormat.format(request.getExpectedDate()), + request.getApplicantName(), purchaseStatusLabel(request.getPurchaseStatus()), + request.getPurchaseBatchNo(), request.getProgressRemark(), request.getProductRemark(), + request.getRequestRemark(), request.getAcceptedQuantity(), request.getPendingArrivalQuantity() + }; + for (int column = 0; column < values.length; column++) + { + setCellValue(row.createCell(column), values[column]); + } + } + + addOptionColumn(workbook, sheet, options, 0, "物料编码", values(products, "code")); + addOptionColumn(workbook, sheet, options, 1, "物料名称", values(products, "name")); + addOptionColumn(workbook, sheet, options, 2, "品牌", values(products, "brand")); + addOptionColumn(workbook, sheet, options, 3, "类别", values(products, "category")); + addOptionColumn(workbook, sheet, options, 4, "参数规格", values(products, "spec")); + addOptionColumn(workbook, sheet, options, 7, "单位", values(products, "unit")); + workbook.setSheetVisibility(workbook.getSheetIndex(options), SheetVisibility.HIDDEN); + workbook.write(response.getOutputStream()); + } + } + + private static void addOptionColumn(XSSFWorkbook workbook, Sheet sheet, Sheet options, + int targetColumn, String title, List values) + { + int optionColumn = options.getRow(0) == null ? 0 : options.getRow(0).getLastCellNum(); + Row titleRow = options.getRow(0); + if (titleRow == null) + { + titleRow = options.createRow(0); + optionColumn = 0; + } + titleRow.createCell(optionColumn).setCellValue(title); + if (values.isEmpty()) + { + return; + } + for (int index = 0; index < values.size(); index++) + { + Row row = options.getRow(index + 1); + if (row == null) + { + row = options.createRow(index + 1); + } + row.createCell(optionColumn).setCellValue(values.get(index)); + } + + String rangeName = "purchase_option_" + targetColumn; + Name name = workbook.createName(); + name.setNameName(rangeName); + String optionColumnName = org.apache.poi.ss.util.CellReference.convertNumToColString(optionColumn); + name.setRefersToFormula("'系统下拉数据'!$" + optionColumnName + "$2:$" + optionColumnName + "$" + (values.size() + 1)); + + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createFormulaListConstraint(rangeName); + CellRangeAddressList range = new CellRangeAddressList(1, 10000, targetColumn, targetColumn); + DataValidation validation = helper.createValidation(constraint, range); + validation.setEmptyCellAllowed(true); + // XSSF writes the inverse OOXML showDropDown flag; true keeps the arrow visible in Excel/WPS. + validation.setSuppressDropDownArrow(true); + validation.setErrorStyle(ErrorStyle.WARNING); + validation.setShowErrorBox(true); + validation.createErrorBox("不在系统数据中", "可以继续输入新值,确认后系统会新增或复用该属性"); + validation.setShowPromptBox(false); + sheet.addValidationData(validation); + } + + private static List values(List products, String field) + { + Set values = new LinkedHashSet<>(); + for (ProductInfo product : products) + { + String value = switch (field) + { + case "code" -> product.getProductCode(); + case "name" -> product.getProductName(); + case "brand" -> product.getBrand(); + case "category" -> product.getCategory(); + case "spec" -> product.getSpec(); + case "unit" -> product.getUnit(); + default -> null; + }; + if (StringUtils.isNotBlank(value)) + { + values.add(value.trim()); + } + } + return new ArrayList<>(values); + } + + private static CellStyle createHeaderStyle(XSSFWorkbook workbook) + { + CellStyle style = workbook.createCellStyle(); + style.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + private static int columnWidth(int column) + { + if (column == 5) + { + return 42 * 256; + } + if (column == 4 || column >= 9) + { + return 24 * 256; + } + return 18 * 256; + } + + private static int exportColumnWidth(int column) + { + if (column == 5) + { + return 42 * 256; + } + if (column == 4 || column == 12 || column == 13 || column == 14) + { + return 24 * 256; + } + return 18 * 256; + } + + private static void setCellValue(Cell cell, Object value) + { + if (value == null) + { + cell.setBlank(); + } + else if (value instanceof Number number) + { + cell.setCellValue(number.doubleValue()); + } + else + { + cell.setCellValue(String.valueOf(value)); + } + } + + private static String purchaseStatusLabel(Integer status) + { + String[] labels = { "待采购", "已下单", "采购中", "部分到货", "已到齐", "已取消" }; + return status != null && status >= 0 && status < labels.length ? labels[status] : "未知"; + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 34990da..4a42acf 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -144,6 +144,13 @@ xss: # WMS agent model config. Keep api-key in environment variables, not in source code. agent: + model: + base-url: ${AGENT_MODEL_BASE_URL:http://192.168.28.10:14081} + api-key: ${AGENT_MODEL_API_KEY:} + model: ${AGENT_MODEL_NAME:Qwen/Qwen3.8-27B-FP8} + timeout-ms: ${AGENT_MODEL_TIMEOUT_MS:8000} + max-output-tokens: ${AGENT_MODEL_MAX_OUTPUT_TOKENS:512} + # 图片批量导入仍使用支持视觉输入的远程模型。 openai: base-url: ${OPENAI_BASE_URL:http://192.168.28.10:18080} api-key: ${OPENAI_API_KEY:sk-dd185946191d42eaa54892dec488bf7b54cad2d7433aee8aac7a017365b0829d} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java index ed7e574..b97b3ed 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.model.LoginUser; @@ -72,8 +74,7 @@ public class TokenService // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); - LoginUser user = redisCache.getCacheObject(userKey); - return user; + return getCachedLoginUser(userKey); } catch (Exception e) { @@ -248,7 +249,7 @@ public class TokenService } for (String key : keys) { - LoginUser loginUser = redisCache.getCacheObject(key); + LoginUser loginUser = getCachedLoginUser(key); if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin()) { // 管理员拥有所有权限,跳过 @@ -267,4 +268,33 @@ public class TokenService log.info("角色[{}]权限变更,已刷新在线用户[{}]的权限缓存", roleId, loginUser.getUsername()); } } + + /** + * DevTools 重启后,旧 Redis 会话可能由不同类加载器反序列化为 JSONObject。 + */ + private LoginUser getCachedLoginUser(String userKey) + { + Object cached = redisCache.getCacheObject(userKey); + if (cached == null) + { + return null; + } + if (cached instanceof LoginUser loginUser) + { + return loginUser; + } + try + { + if (cached instanceof JSONObject jsonObject) + { + return jsonObject.toJavaObject(LoginUser.class); + } + return JSON.parseObject(JSON.toJSONString(cached), LoginUser.class); + } + catch (Exception e) + { + log.warn("Redis 登录会话[{}]格式无效,已忽略:{}", userKey, e.getMessage()); + return null; + } + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/InRecord.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/InRecord.java index 33ec1b5..a23f349 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/InRecord.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/InRecord.java @@ -22,6 +22,13 @@ public class InRecord extends BaseEntity private Long productId; + private Long purchaseRequestId; + + private Long arrivalId; + + @Excel(name = "来源单号") + private String sourceNo; + @Excel(name = "入库类型", readConverterExp = "1=生产入库,2=采购入库,3=归还入库,4=退货入库") private Integer inType; @@ -75,6 +82,12 @@ public class InRecord extends BaseEntity @NotNull(message = "产品不能为空") public Long getProductId() { return productId; } public void setProductId(Long productId) { this.productId = productId; } + public Long getPurchaseRequestId() { return purchaseRequestId; } + public void setPurchaseRequestId(Long purchaseRequestId) { this.purchaseRequestId = purchaseRequestId; } + public Long getArrivalId() { return arrivalId; } + public void setArrivalId(Long arrivalId) { this.arrivalId = arrivalId; } + public String getSourceNo() { return sourceNo; } + public void setSourceNo(String sourceNo) { this.sourceNo = sourceNo; } @NotNull(message = "入库类型不能为空") public Integer getInType() { return inType; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrival.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrival.java new file mode 100644 index 0000000..26fba4f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrival.java @@ -0,0 +1,96 @@ +package com.ruoyi.warehouse.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; + +public class PurchaseArrival extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long id; + private String arrivalNo; + private Long purchaseRequestId; + private BigDecimal arrivalQuantity; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date arrivalDate; + + private Integer acceptanceStatus; + private String acceptanceRemark; + private String acceptedBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date acceptedTime; + + private Integer preAcceptPurchaseStatus; + private String revokedBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date revokedTime; + + private String revokeRemark; + + private String requestNo; + private Long applicantId; + private String applicantName; + private Long productId; + private String productCode; + private String materialName; + private String materialBrand; + private String materialParams; + private String unit; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getArrivalNo() { return arrivalNo; } + public void setArrivalNo(String arrivalNo) { this.arrivalNo = arrivalNo; } + public Long getPurchaseRequestId() { return purchaseRequestId; } + public void setPurchaseRequestId(Long purchaseRequestId) { this.purchaseRequestId = purchaseRequestId; } + + @NotNull(message = "到货数量不能为空") + @DecimalMin(value = "0.001", message = "到货数量必须大于0") + public BigDecimal getArrivalQuantity() { return arrivalQuantity; } + public void setArrivalQuantity(BigDecimal arrivalQuantity) { this.arrivalQuantity = arrivalQuantity; } + + @NotNull(message = "到货日期不能为空") + public Date getArrivalDate() { return arrivalDate; } + public void setArrivalDate(Date arrivalDate) { this.arrivalDate = arrivalDate; } + public Integer getAcceptanceStatus() { return acceptanceStatus; } + public void setAcceptanceStatus(Integer acceptanceStatus) { this.acceptanceStatus = acceptanceStatus; } + public String getAcceptanceRemark() { return acceptanceRemark; } + public void setAcceptanceRemark(String acceptanceRemark) { this.acceptanceRemark = acceptanceRemark; } + public String getAcceptedBy() { return acceptedBy; } + public void setAcceptedBy(String acceptedBy) { this.acceptedBy = acceptedBy; } + public Date getAcceptedTime() { return acceptedTime; } + public void setAcceptedTime(Date acceptedTime) { this.acceptedTime = acceptedTime; } + public Integer getPreAcceptPurchaseStatus() { return preAcceptPurchaseStatus; } + public void setPreAcceptPurchaseStatus(Integer preAcceptPurchaseStatus) { this.preAcceptPurchaseStatus = preAcceptPurchaseStatus; } + public String getRevokedBy() { return revokedBy; } + public void setRevokedBy(String revokedBy) { this.revokedBy = revokedBy; } + public Date getRevokedTime() { return revokedTime; } + public void setRevokedTime(Date revokedTime) { this.revokedTime = revokedTime; } + public String getRevokeRemark() { return revokeRemark; } + public void setRevokeRemark(String revokeRemark) { this.revokeRemark = revokeRemark; } + public String getRequestNo() { return requestNo; } + public void setRequestNo(String requestNo) { this.requestNo = requestNo; } + public Long getApplicantId() { return applicantId; } + public void setApplicantId(Long applicantId) { this.applicantId = applicantId; } + public String getApplicantName() { return applicantName; } + public void setApplicantName(String applicantName) { this.applicantName = applicantName; } + public Long getProductId() { return productId; } + public void setProductId(Long productId) { this.productId = productId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + public String getMaterialName() { return materialName; } + public void setMaterialName(String materialName) { this.materialName = materialName; } + public String getMaterialBrand() { return materialBrand; } + public void setMaterialBrand(String materialBrand) { this.materialBrand = materialBrand; } + public String getMaterialParams() { return materialParams; } + public void setMaterialParams(String materialParams) { this.materialParams = materialParams; } + public String getUnit() { return unit; } + public void setUnit(String unit) { this.unit = unit; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalAcceptanceRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalAcceptanceRequest.java new file mode 100644 index 0000000..991a425 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalAcceptanceRequest.java @@ -0,0 +1,15 @@ +package com.ruoyi.warehouse.domain; + +import jakarta.validation.constraints.NotNull; + +public class PurchaseArrivalAcceptanceRequest +{ + @NotNull(message = "验收结果不能为空") + private Integer acceptanceStatus; + private String acceptanceRemark; + + public Integer getAcceptanceStatus() { return acceptanceStatus; } + public void setAcceptanceStatus(Integer acceptanceStatus) { this.acceptanceStatus = acceptanceStatus; } + public String getAcceptanceRemark() { return acceptanceRemark; } + public void setAcceptanceRemark(String acceptanceRemark) { this.acceptanceRemark = acceptanceRemark; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalRevokeRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalRevokeRequest.java new file mode 100644 index 0000000..fcdd30b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseArrivalRevokeRequest.java @@ -0,0 +1,19 @@ +package com.ruoyi.warehouse.domain; + +import jakarta.validation.constraints.NotBlank; + +public class PurchaseArrivalRevokeRequest +{ + private String revokeRemark; + + @NotBlank(message = "撤销原因不能为空") + public String getRevokeRemark() + { + return revokeRemark; + } + + public void setRevokeRemark(String revokeRemark) + { + this.revokeRemark = revokeRemark; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatch.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatch.java new file mode 100644 index 0000000..128b7e7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatch.java @@ -0,0 +1,28 @@ +package com.ruoyi.warehouse.domain; + +import com.ruoyi.common.core.domain.BaseEntity; + +public class PurchaseBatch extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long id; + private String batchNo; + private String batchName; + private Integer purchaseStatus; + private String progressRemark; + private Integer requestCount; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getBatchNo() { return batchNo; } + public void setBatchNo(String batchNo) { this.batchNo = batchNo; } + public String getBatchName() { return batchName; } + public void setBatchName(String batchName) { this.batchName = batchName; } + public Integer getPurchaseStatus() { return purchaseStatus; } + public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; } + public String getProgressRemark() { return progressRemark; } + public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; } + public Integer getRequestCount() { return requestCount; } + public void setRequestCount(Integer requestCount) { this.requestCount = requestCount; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatchAssignRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatchAssignRequest.java new file mode 100644 index 0000000..b6a9afb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseBatchAssignRequest.java @@ -0,0 +1,12 @@ +package com.ruoyi.warehouse.domain; + +public class PurchaseBatchAssignRequest +{ + private Long[] requestIds; + private Long batchId; + + public Long[] getRequestIds() { return requestIds; } + public void setRequestIds(Long[] requestIds) { this.requestIds = requestIds; } + public Long getBatchId() { return batchId; } + public void setBatchId(Long batchId) { this.batchId = batchId; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseRequest.java new file mode 100644 index 0000000..7016114 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseRequest.java @@ -0,0 +1,144 @@ +package com.ruoyi.warehouse.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class PurchaseRequest extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long id; + + private String requestNo; + + private Long productId; + + private Long purchaseBatchId; + + @Excel(name = "关联物料编码", prompt = "已有物料建议填写准确编码;留空时按名称、品牌、类别、参数规格匹配") + private String productCode; + + @Excel(name = "物料名称", prompt = "必填;无匹配物料时将自动新增产品信息") + private String materialName; + + @Excel(name = "品牌") + private String materialBrand; + + @Excel(name = "类别") + private String category; + + @Excel(name = "参数规格") + private String materialParams; + + @Excel(name = "采购链接") + private String purchaseLink; + + @Excel(name = "需求数量") + private BigDecimal quantity; + + @Excel(name = "单位") + private String unit; + + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "期望到货日期", width = 20, dateFormat = "yyyy-MM-dd") + private Date expectedDate; + + private Long applicantId; + + @Excel(name = "申请人", type = Excel.Type.EXPORT) + private String applicantName; + + @Excel(name = "采购状态", readConverterExp = "0=待采购,1=已下单,2=采购中,3=部分到货,4=已到齐,5=已取消", type = Excel.Type.EXPORT) + private Integer purchaseStatus; + + @Excel(name = "采购批次", type = Excel.Type.EXPORT) + private String purchaseBatchNo; + + @Excel(name = "进度说明", type = Excel.Type.EXPORT) + private String progressRemark; + + @Excel(name = "产品备注") + private String productRemark; + + @Excel(name = "申请备注") + private String requestRemark; + + private String productName; + private String productBrand; + private String productCategory; + private String productSpec; + + @Excel(name = "已验收数量", type = Excel.Type.EXPORT) + private BigDecimal acceptedQuantity; + + @Excel(name = "待验收数量", type = Excel.Type.EXPORT) + private BigDecimal pendingArrivalQuantity; + + private BigDecimal remainingQuantity; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getRequestNo() { return requestNo; } + public void setRequestNo(String requestNo) { this.requestNo = requestNo; } + public Long getProductId() { return productId; } + public void setProductId(Long productId) { this.productId = productId; } + public Long getPurchaseBatchId() { return purchaseBatchId; } + public void setPurchaseBatchId(Long purchaseBatchId) { this.purchaseBatchId = purchaseBatchId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + + @NotBlank(message = "物料名称不能为空") + public String getMaterialName() { return materialName; } + public void setMaterialName(String materialName) { this.materialName = materialName; } + public String getMaterialBrand() { return materialBrand; } + public void setMaterialBrand(String materialBrand) { this.materialBrand = materialBrand; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getMaterialParams() { return materialParams; } + public void setMaterialParams(String materialParams) { this.materialParams = materialParams; } + public String getPurchaseLink() { return purchaseLink; } + public void setPurchaseLink(String purchaseLink) { this.purchaseLink = purchaseLink; } + + @NotNull(message = "需求数量不能为空") + @DecimalMin(value = "0.001", message = "需求数量必须大于0") + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public String getUnit() { return unit; } + public void setUnit(String unit) { this.unit = unit; } + public Date getExpectedDate() { return expectedDate; } + public void setExpectedDate(Date expectedDate) { this.expectedDate = expectedDate; } + public Long getApplicantId() { return applicantId; } + public void setApplicantId(Long applicantId) { this.applicantId = applicantId; } + public String getApplicantName() { return applicantName; } + public void setApplicantName(String applicantName) { this.applicantName = applicantName; } + public Integer getPurchaseStatus() { return purchaseStatus; } + public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; } + public String getPurchaseBatchNo() { return purchaseBatchNo; } + public void setPurchaseBatchNo(String purchaseBatchNo) { this.purchaseBatchNo = purchaseBatchNo; } + public String getProgressRemark() { return progressRemark; } + public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; } + public String getProductRemark() { return productRemark; } + public void setProductRemark(String productRemark) { this.productRemark = productRemark; } + public String getRequestRemark() { return requestRemark; } + public void setRequestRemark(String requestRemark) { this.requestRemark = requestRemark; } + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + public String getProductBrand() { return productBrand; } + public void setProductBrand(String productBrand) { this.productBrand = productBrand; } + public String getProductCategory() { return productCategory; } + public void setProductCategory(String productCategory) { this.productCategory = productCategory; } + public String getProductSpec() { return productSpec; } + public void setProductSpec(String productSpec) { this.productSpec = productSpec; } + public BigDecimal getAcceptedQuantity() { return acceptedQuantity; } + public void setAcceptedQuantity(BigDecimal acceptedQuantity) { this.acceptedQuantity = acceptedQuantity; } + public BigDecimal getPendingArrivalQuantity() { return pendingArrivalQuantity; } + public void setPendingArrivalQuantity(BigDecimal pendingArrivalQuantity) { this.pendingArrivalQuantity = pendingArrivalQuantity; } + public BigDecimal getRemainingQuantity() { return remainingQuantity; } + public void setRemainingQuantity(BigDecimal remainingQuantity) { this.remainingQuantity = remainingQuantity; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseStatusUpdateRequest.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseStatusUpdateRequest.java new file mode 100644 index 0000000..eb61548 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/domain/PurchaseStatusUpdateRequest.java @@ -0,0 +1,15 @@ +package com.ruoyi.warehouse.domain; + +import jakarta.validation.constraints.NotNull; + +public class PurchaseStatusUpdateRequest +{ + @NotNull(message = "采购状态不能为空") + private Integer purchaseStatus; + private String progressRemark; + + public Integer getPurchaseStatus() { return purchaseStatus; } + public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; } + public String getProgressRemark() { return progressRemark; } + public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/InRecordMapper.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/InRecordMapper.java index 225bd3f..bf54096 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/InRecordMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/InRecordMapper.java @@ -11,6 +11,8 @@ public interface InRecordMapper public InRecord selectInRecordByIdForUpdate(Long id); + public InRecord selectInRecordByArrivalIdForUpdate(Long arrivalId); + public InRecord checkInNoUnique(String inNo); public int insertInRecord(InRecord inRecord); @@ -20,4 +22,6 @@ public interface InRecordMapper public int confirmInRecord(InRecord inRecord); public int deleteInRecordByIds(Long[] ids); + + public int deletePendingInRecordByArrivalId(Long arrivalId); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseArrivalMapper.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseArrivalMapper.java new file mode 100644 index 0000000..1d481f1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseArrivalMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.warehouse.mapper; + +import java.math.BigDecimal; +import java.util.List; +import com.ruoyi.warehouse.domain.PurchaseArrival; + +public interface PurchaseArrivalMapper +{ + List selectPurchaseArrivalList(Long purchaseRequestId); + PurchaseArrival selectPurchaseArrivalById(Long id); + PurchaseArrival selectPurchaseArrivalByIdForUpdate(Long id); + BigDecimal selectActiveQuantityByRequestId(Long purchaseRequestId); + BigDecimal selectAcceptedQuantityByRequestId(Long purchaseRequestId); + int countByRequestId(Long purchaseRequestId); + int insertPurchaseArrival(PurchaseArrival arrival); + int updatePurchaseArrivalAcceptance(PurchaseArrival arrival); + int revokePurchaseArrivalAcceptance(PurchaseArrival arrival); + int deletePendingPurchaseArrival(Long id); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseBatchMapper.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseBatchMapper.java new file mode 100644 index 0000000..4a3f1f6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseBatchMapper.java @@ -0,0 +1,22 @@ +package com.ruoyi.warehouse.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.warehouse.domain.PurchaseBatch; + +public interface PurchaseBatchMapper +{ + List selectPurchaseBatchList(PurchaseBatch batch); + List selectPurchaseBatchOptions(@Param("keyword") String keyword); + List selectPurchaseBatchFilterOptions(@Param("keyword") String keyword); + PurchaseBatch selectPurchaseBatchById(Long id); + PurchaseBatch selectPurchaseBatchByNo(String batchNo); + int insertPurchaseBatch(PurchaseBatch batch); + int updatePurchaseBatch(PurchaseBatch batch); + int countRequestsByBatchId(Long batchId); + int countRequestsByIds(Long[] requestIds); + int assignRequests(@Param("requestIds") Long[] requestIds, @Param("batch") PurchaseBatch batch, @Param("updateBy") String updateBy); + int syncRequestBatchNo(@Param("batchId") Long batchId, @Param("batchNo") String batchNo, + @Param("updateBy") String updateBy); + int deletePurchaseBatchById(Long id); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseRequestMapper.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseRequestMapper.java new file mode 100644 index 0000000..74a97e3 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/mapper/PurchaseRequestMapper.java @@ -0,0 +1,18 @@ +package com.ruoyi.warehouse.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.warehouse.domain.PurchaseRequest; + +public interface PurchaseRequestMapper +{ + List selectPurchaseRequestList(PurchaseRequest purchaseRequest); + PurchaseRequest selectPurchaseRequestById(@Param("id") Long id, @Param("applicantId") Long applicantId); + PurchaseRequest selectPurchaseRequestByIdForUpdate(Long id); + PurchaseRequest checkRequestNoUnique(String requestNo); + int insertPurchaseRequest(PurchaseRequest purchaseRequest); + int updatePurchaseRequest(@Param("purchaseRequest") PurchaseRequest purchaseRequest, @Param("applicantId") Long applicantId); + int deletePurchaseRequestByIds(@Param("ids") Long[] ids, @Param("applicantId") Long applicantId); + int updatePurchaseStatus(@Param("id") Long id, @Param("purchaseStatus") Integer purchaseStatus, + @Param("progressRemark") String progressRemark, @Param("updateBy") String updateBy); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseArrivalService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseArrivalService.java new file mode 100644 index 0000000..1260df8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseArrivalService.java @@ -0,0 +1,15 @@ +package com.ruoyi.warehouse.service; + +import java.util.List; +import com.ruoyi.warehouse.domain.PurchaseArrival; +import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest; +import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest; + +public interface IPurchaseArrivalService +{ + List selectPurchaseArrivalList(Long purchaseRequestId); + int insertPurchaseArrival(PurchaseArrival arrival); + int acceptPurchaseArrival(Long id, PurchaseArrivalAcceptanceRequest request); + int revokePurchaseArrivalAcceptance(Long id, PurchaseArrivalRevokeRequest request); + int deletePurchaseArrival(Long id); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseBatchService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseBatchService.java new file mode 100644 index 0000000..3c7c954 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseBatchService.java @@ -0,0 +1,16 @@ +package com.ruoyi.warehouse.service; + +import java.util.List; +import com.ruoyi.warehouse.domain.PurchaseBatch; +import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest; + +public interface IPurchaseBatchService +{ + List selectPurchaseBatchList(PurchaseBatch batch); + List selectPurchaseBatchOptions(String keyword); + List selectPurchaseBatchFilterOptions(String keyword); + int insertPurchaseBatch(PurchaseBatch batch); + int updatePurchaseBatch(PurchaseBatch batch); + int assignPurchaseBatch(PurchaseBatchAssignRequest request); + int deletePurchaseBatchById(Long id); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseRequestService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseRequestService.java new file mode 100644 index 0000000..21a400e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseRequestService.java @@ -0,0 +1,17 @@ +package com.ruoyi.warehouse.service; + +import java.util.List; +import com.ruoyi.warehouse.domain.PurchaseRequest; +import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest; + +public interface IPurchaseRequestService +{ + List selectPurchaseRequestList(PurchaseRequest purchaseRequest); + PurchaseRequest selectPurchaseRequestById(Long id); + int insertPurchaseRequest(PurchaseRequest purchaseRequest); + int updatePurchaseRequest(PurchaseRequest purchaseRequest); + int deletePurchaseRequestByIds(Long[] ids); + int updatePurchaseStatus(Long id, PurchaseStatusUpdateRequest request); + int batchInsertPurchaseRequest(List requestList); + String importPurchaseRequest(List requestList, String username); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseSmartImportService.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseSmartImportService.java new file mode 100644 index 0000000..2030528 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/IPurchaseSmartImportService.java @@ -0,0 +1,9 @@ +package com.ruoyi.warehouse.service; + +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSONObject; + +public interface IPurchaseSmartImportService +{ + JSONObject analyze(MultipartFile file) throws Exception; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java index 46c49ed..e5b3a12 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/AgentServiceImpl.java @@ -6,6 +6,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.text.Normalizer; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; @@ -46,6 +47,11 @@ import com.ruoyi.warehouse.service.IWarehouseAreaService; @Service public class AgentServiceImpl implements IAgentService { + private static final HttpClient MODEL_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(3)) + .version(HttpClient.Version.HTTP_1_1) + .build(); + 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"; @@ -55,19 +61,33 @@ public class AgentServiceImpl implements IAgentService 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*(个|条|项)?"); + private static final Pattern REFERENCE_PATTERN = Pattern.compile("上一个|上一条|刚才(?:的)?(?:那个|那条|那项)|刚刚(?:的)?(?:那个|那条|那项)|这个|那个|它|上述|上面(?:的)?"); + private static final Pattern QUANTITY_WITH_UNIT_PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)\\s*(个|件|箱|支|台|套|包|卷|瓶|袋|张|本)"); + private static final Pattern CHINESE_QUANTITY_PATTERN = Pattern.compile("([一二两三四五六七八九十]+)\\s*(个|件|箱|支|台|套|包|卷|瓶|袋|张|本)"); + private static final Pattern ACTION_QUANTITY_PATTERN = Pattern.compile("(?:入库|出库|盘库|盘点)\\s*(?:数量\\s*[::]?\\s*)?([0-9]+(?:\\.[0-9]+)?)"); + private static final Pattern NAMED_QUANTITY_PATTERN = Pattern.compile("数量\\s*[::]?\\s*([0-9]+(?:\\.[0-9]+)?)"); + private static final Pattern PORT_SPEC_PATTERN = Pattern.compile("([0-90-9]+)\\s*(?:进|入)\\s*([0-90-9]+)\\s*出", Pattern.CASE_INSENSITIVE); - @Value("${agent.openai.base-url}") + @Value("${agent.model.base-url}") private String baseUrl; - @Value("${agent.openai.api-key}") + @Value("${agent.model.api-key:}") private String apiKey; - @Value("${agent.openai.model}") + @Value("${agent.model.model}") private String model; + @Value("${agent.model.timeout-ms:8000}") + private long modelTimeoutMs; + + @Value("${agent.model.max-output-tokens:512}") + private int maxOutputTokens; + + private volatile List categoryCache = List.of(); + private volatile long categoryCacheExpiresAt; + @Autowired private IWarehouseAreaService areaService; @@ -99,15 +119,15 @@ public class AgentServiceImpl implements IAgentService AgentPlan plan = null; List warnings = new ArrayList<>(); - if (StringUtils.isBlank(apiKey)) + if (StringUtils.isBlank(baseUrl)) { - warnings.add("未配置 OPENAI_API_KEY,已使用基础规则解析"); + warnings.add("未配置大模型地址,已使用基础规则解析"); } else { try { - plan = callModel(text, contextResults); + plan = callModel(text, hasExplicitReference(text) ? contextResults : null); } catch (Exception e) { @@ -247,10 +267,13 @@ public class AgentServiceImpl implements IAgentService { plan.setAction(ACTION_UNKNOWN); } - if (requiresOperator(plan.getAction()) && StringUtils.isBlank(plan.getOperatorName())) + resolveExplicitAction(plan, originalText); + resolveOperator(plan, originalText, username); + + if (requiresOperator(plan.getAction()) && originalText != null) { - plan.setOperatorName(resolveNickName(username)); - plan.getWarnings().add("未识别到人员,默认使用当前登录用户"); + // 数量必须来自本次原文的明确数量表达,不能使用模型从规格中猜出的数字。 + plan.setQuantity(extractQuantity(originalText)); } if (ACTION_CREATE_IN.equals(plan.getAction()) && plan.getInType() == null) @@ -373,25 +396,27 @@ public class AgentServiceImpl implements IAgentService { 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); + JSONArray messages = new JSONArray(); + messages.add(message("system", systemPrompt())); + messages.add(message("user", buildUserInput(text, contextResults))); + body.put("messages", messages); + body.put("response_format", JSONObject.of("type", "json_object")); + body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false)); + body.put("temperature", 0); + body.put("max_tokens", maxOutputTokens); + body.put("stream", false); - HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); - HttpRequest request = HttpRequest.newBuilder() + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(normalizeEndpoint(baseUrl))) - .timeout(Duration.ofSeconds(30)) - .header("Authorization", "Bearer " + apiKey) + .timeout(Duration.ofMillis(modelTimeoutMs)) .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)); + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8)); + if (StringUtils.isNotBlank(apiKey)) + { + requestBuilder.header("Authorization", "Bearer " + apiKey); + } + HttpResponse response = MODEL_CLIENT.send(requestBuilder.build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new ServiceException("HTTP " + response.statusCode()); @@ -417,7 +442,9 @@ public class AgentServiceImpl implements IAgentService 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 开始的序号。" + + "只有原文明说第几个、第2条、上一个、刚才那个、这个、那个或它时,referenceIndex 才能返回从 1 开始的序号;‘一个’、‘1个’表示数量,不是引用,没有明确指代必须返回 null。" + + "quantity 只表示本次入库、出库或盘点的数量,规格型号里的数字不算数量,‘一个’表示数量 1。" + + "类似‘2进6出’、‘8口’的描述优先放入 spec,不要把人员姓名、‘东西’、‘产品’放入 productName。" + "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。" + "出库类型 outType:1生产出库,2退货出库,3销售出库。" + "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;如果用户未提供 category,优先从 existingCategories 选择合适类别,不合适时推断一个简短合理的类别。" @@ -435,25 +462,40 @@ public class AgentServiceImpl implements IAgentService private String normalizeEndpoint(String configuredBaseUrl) { - String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim(); + String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:14081").trim(); while (url.endsWith("/")) { url = url.substring(0, url.length() - 1); } if (url.endsWith("/v1")) { - return url + "/responses"; + return url + "/chat/completions"; } - if (url.endsWith("/v1/responses")) + if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions")) { return url; } - return url + "/v1/responses"; + if (url.endsWith("/v1/responses")) + { + return url.substring(0, url.length() - "/responses".length()) + "/chat/completions"; + } + return url + "/v1/chat/completions"; } private String extractOutputText(String responseBody) { JSONObject root = JSON.parseObject(responseBody); + JSONArray choices = root.getJSONArray("choices"); + if (choices != null && !choices.isEmpty()) + { + JSONObject choice = choices.getJSONObject(0); + JSONObject responseMessage = choice == null ? null : choice.getJSONObject("message"); + String content = responseMessage == null ? null : responseMessage.getString("content"); + if (StringUtils.isNotBlank(content)) + { + return content; + } + } String outputText = root.getString("output_text"); if (StringUtils.isNotBlank(outputText)) { @@ -704,17 +746,35 @@ public class AgentServiceImpl implements IAgentService private BigDecimal extractQuantity(String text) { - Matcher matcher = NUMBER_PATTERN.matcher(StringUtils.defaultString(text)); - BigDecimal quantity = null; + String source = StringUtils.defaultString(text); + Matcher matcher = NAMED_QUANTITY_PATTERN.matcher(source); + if (matcher.find()) + { + return new BigDecimal(matcher.group(1)); + } + matcher = QUANTITY_WITH_UNIT_PATTERN.matcher(source); while (matcher.find()) { - if (matcher.start() > 0 && text.charAt(matcher.start() - 1) == '第') + if (matcher.start() == 0 || source.charAt(matcher.start() - 1) != '第') { - continue; + return new BigDecimal(matcher.group(1)); } - quantity = new BigDecimal(matcher.group(1)); } - return quantity; + matcher = CHINESE_QUANTITY_PATTERN.matcher(source); + while (matcher.find()) + { + if (matcher.start() == 0 || source.charAt(matcher.start() - 1) != '第') + { + Integer value = parseIndex(matcher.group(1)); + return value == null ? null : BigDecimal.valueOf(value); + } + } + matcher = ACTION_QUANTITY_PATTERN.matcher(source); + if (matcher.find()) + { + return new BigDecimal(matcher.group(1)); + } + return null; } private String extractInNo(String text) @@ -865,12 +925,29 @@ public class AgentServiceImpl implements IAgentService private void resolveReferencedResult(AgentPlan plan, String originalText, List contextResults) { - if (plan.getReferenceIndex() == null) + if (originalText == null) { - plan.setReferenceIndex(extractReferenceIndex(originalText)); + return; } - if (plan.getReferenceIndex() == null) + Integer explicitIndex = extractReferenceIndex(originalText); + if (!hasExplicitReference(originalText)) { + // 不信任模型自行推断的序号,防止把“出库一个”理解成“出库第一个”。 + plan.setReferenceIndex(null); + return; + } + if (explicitIndex != null) + { + plan.setReferenceIndex(explicitIndex); + } + else if (contextResults != null && contextResults.size() == 1) + { + plan.setReferenceIndex(1); + } + else + { + plan.setReferenceIndex(null); + plan.setMessage("上次结果有多项,请明确说第几个"); return; } if (contextResults == null || contextResults.isEmpty()) @@ -992,6 +1069,25 @@ public class AgentServiceImpl implements IAgentService } private List existingCategories() + { + long now = System.currentTimeMillis(); + if (now < categoryCacheExpiresAt) + { + return categoryCache; + } + synchronized (this) + { + if (now < categoryCacheExpiresAt) + { + return categoryCache; + } + categoryCache = loadExistingCategories(); + categoryCacheExpiresAt = now + 60_000L; + return categoryCache; + } + } + + private List loadExistingCategories() { Set categories = new LinkedHashSet<>(); for (ProductInfo product : productService.selectProductInfoAll()) @@ -1016,6 +1112,94 @@ public class AgentServiceImpl implements IAgentService || ACTION_CREATE_CHECK.equals(action); } + private boolean hasExplicitReference(String text) + { + return extractReferenceIndex(text) != null || REFERENCE_PATTERN.matcher(StringUtils.defaultString(text)).find(); + } + + private void resolveExplicitAction(AgentPlan plan, String originalText) + { + if (StringUtils.isBlank(originalText)) + { + return; + } + if ((originalText.contains("添加") || originalText.contains("新增") || originalText.contains("录入")) + && originalText.contains("库区")) + { + plan.setAction(ACTION_CREATE_AREA); + } + else if ((originalText.contains("添加") || originalText.contains("新增") || originalText.contains("录入")) + && (originalText.contains("产品") || originalText.contains("物料"))) + { + plan.setAction(ACTION_CREATE_PRODUCT); + } + else if (originalText.contains("确认") && originalText.contains("入库")) + { + plan.setAction(ACTION_CONFIRM_IN); + } + else if (originalText.contains("出库")) + { + plan.setAction(ACTION_CREATE_OUT); + } + else if (originalText.contains("盘库") || originalText.contains("盘点")) + { + plan.setAction(ACTION_CREATE_CHECK); + } + else if (originalText.contains("入库")) + { + plan.setAction(ACTION_CREATE_IN); + } + } + + private void resolveOperator(AgentPlan plan, String originalText, String username) + { + if (!requiresOperator(plan.getAction())) + { + return; + } + if (StringUtils.isNotBlank(originalText)) + { + SysUser matchedUser = null; + int matchedLength = 0; + SysUser query = new SysUser(); + query.setStatus("0"); + for (SysUser user : userService.selectUserList(query)) + { + String matchedName = matchedUserName(originalText, user); + if (StringUtils.isNotBlank(matchedName) && matchedName.length() > matchedLength) + { + matchedUser = user; + matchedLength = matchedName.length(); + } + } + if (matchedUser != null) + { + plan.setOperatorName(StringUtils.defaultIfBlank(matchedUser.getNickName(), matchedUser.getUserName())); + return; + } + } + if (StringUtils.isBlank(plan.getOperatorName())) + { + plan.setOperatorName(resolveNickName(username)); + plan.getWarnings().add("未识别到人员,默认使用当前登录用户"); + } + } + + private String matchedUserName(String text, SysUser user) + { + String nickName = user == null ? null : user.getNickName(); + String userName = user == null ? null : user.getUserName(); + if (StringUtils.isNotBlank(nickName) && text.contains(nickName)) + { + return nickName; + } + if (StringUtils.isNotBlank(userName) && text.toLowerCase(Locale.ROOT).contains(userName.toLowerCase(Locale.ROOT))) + { + return userName; + } + return null; + } + private boolean noAreaCandidates(AgentPlan plan) { for (AgentCandidate candidate : plan.getCandidates()) @@ -1150,12 +1334,23 @@ public class AgentServiceImpl implements IAgentService private int productScore(AgentPlan plan, String originalText, ProductInfo product) { + String requestedPortSpec = extractPortSpec(originalText); + String productPortSpec = extractPortSpec(product.getSpec()); + if (StringUtils.isNotBlank(requestedPortSpec) && StringUtils.isNotBlank(productPortSpec) + && !requestedPortSpec.equals(productPortSpec)) + { + return 0; + } 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.getSpec(), product.getSpec(), 14); score += fieldScore(plan.getUnit(), product.getUnit(), 1); + if (StringUtils.isNotBlank(requestedPortSpec) && requestedPortSpec.equals(productPortSpec)) + { + score += 24; + } String fullInfo = productFullText(product); if (containsIgnoreCase(originalText, fullInfo) || containsIgnoreCase(fullInfo, originalText)) { @@ -1166,7 +1361,7 @@ public class AgentServiceImpl implements IAgentService 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; + score += containsNormalized(originalText, product.getSpec()) ? 10 : 0; } return score; } @@ -1177,11 +1372,11 @@ public class AgentServiceImpl implements IAgentService { return 0; } - if (input.trim().equalsIgnoreCase(target.trim())) + if (normalizeSearchText(input).equals(normalizeSearchText(target))) { return exactScore; } - if (containsIgnoreCase(input, target) || containsIgnoreCase(target, input)) + if (containsNormalized(input, target) || containsNormalized(target, input)) { return Math.max(1, exactScore / 2); } @@ -1218,6 +1413,38 @@ public class AgentServiceImpl implements IAgentService return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT)); } + private boolean containsNormalized(String text, String keyword) + { + String normalizedText = normalizeSearchText(text); + String normalizedKeyword = normalizeSearchText(keyword); + return StringUtils.isNotBlank(normalizedText) && StringUtils.isNotBlank(normalizedKeyword) + && normalizedText.contains(normalizedKeyword); + } + + private String normalizeSearchText(String value) + { + if (StringUtils.isBlank(value)) + { + return ""; + } + return Normalizer.normalize(value, Normalizer.Form.NFKC) + .toLowerCase(Locale.ROOT) + .replace('×', 'x') + .replace('*', '*') + .replaceAll("[\\s\\p{Punct},。;:、]+", ""); + } + + private String extractPortSpec(String text) + { + String source = Normalizer.normalize(StringUtils.defaultString(text), Normalizer.Form.NFKC); + Matcher matcher = PORT_SPEC_PATTERN.matcher(source); + if (!matcher.find()) + { + return null; + } + return matcher.group(1) + "进" + matcher.group(2) + "出"; + } + private void fillProduct(AgentPlan plan, ProductInfo product) { plan.setProductId(product.getId()); diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/InRecordServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/InRecordServiceImpl.java index 021e372..24e1b70 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/InRecordServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/InRecordServiceImpl.java @@ -78,6 +78,16 @@ public class InRecordServiceImpl implements IInRecordService { throw new ServiceException("已入库记录不能修改"); } + if (dbRecord != null && dbRecord.getArrivalId() != null) + { + inRecord.setInNo(dbRecord.getInNo()); + inRecord.setProductId(dbRecord.getProductId()); + inRecord.setPurchaseRequestId(dbRecord.getPurchaseRequestId()); + inRecord.setArrivalId(dbRecord.getArrivalId()); + inRecord.setSourceNo(dbRecord.getSourceNo()); + inRecord.setInType(dbRecord.getInType()); + inRecord.setInNum(dbRecord.getInNum()); + } return inRecordMapper.updateInRecord(inRecord); } @@ -91,6 +101,10 @@ public class InRecordServiceImpl implements IInRecordService { throw new ServiceException("已入库记录不能删除"); } + if (dbRecord != null && dbRecord.getArrivalId() != null) + { + throw new ServiceException("采购验收生成的待入库记录不能删除"); + } } return inRecordMapper.deleteInRecordByIds(ids); } @@ -108,6 +122,15 @@ public class InRecordServiceImpl implements IInRecordService { throw new ServiceException("该入库记录已确认"); } + validateInRecord(inRecord); + if (inRecord.getAreaId() == null) + { + throw new ServiceException("请先选择库区再确认入库"); + } + if (inRecord.getProductId() == null) + { + throw new ServiceException("入库产品不能为空"); + } stockInfoMapper.upsertIncreaseStock(inRecord.getAreaId(), inRecord.getProductId(), inRecord.getInNum()); InRecord confirmRecord = new InRecord(); confirmRecord.setId(id); diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseArrivalServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseArrivalServiceImpl.java new file mode 100644 index 0000000..8b4bc33 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseArrivalServiceImpl.java @@ -0,0 +1,274 @@ +package com.ruoyi.warehouse.service.impl; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.warehouse.domain.InRecord; +import com.ruoyi.warehouse.domain.PurchaseArrival; +import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest; +import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest; +import com.ruoyi.warehouse.domain.PurchaseRequest; +import com.ruoyi.warehouse.mapper.InRecordMapper; +import com.ruoyi.warehouse.mapper.PurchaseArrivalMapper; +import com.ruoyi.warehouse.mapper.PurchaseRequestMapper; +import com.ruoyi.warehouse.service.IPurchaseArrivalService; + +@Service +public class PurchaseArrivalServiceImpl implements IPurchaseArrivalService +{ + private static final AtomicInteger ARRIVAL_SEQUENCE = new AtomicInteger(); + + @Autowired + private PurchaseArrivalMapper purchaseArrivalMapper; + + @Autowired + private PurchaseRequestMapper purchaseRequestMapper; + + @Autowired + private InRecordMapper inRecordMapper; + + @Override + public List selectPurchaseArrivalList(Long purchaseRequestId) + { + if (purchaseRequestMapper.selectPurchaseRequestById(purchaseRequestId, null) == null) + { + throw new ServiceException("采购申请不存在"); + } + return purchaseArrivalMapper.selectPurchaseArrivalList(purchaseRequestId); + } + + @Override + @Transactional + public int insertPurchaseArrival(PurchaseArrival arrival) + { + if (arrival == null || arrival.getPurchaseRequestId() == null) + { + throw new ServiceException("采购申请不能为空"); + } + PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId()); + requireOwnerOrManager(request); + if (Integer.valueOf(5).equals(request.getPurchaseStatus())) + { + throw new ServiceException("已取消的采购申请不能登记到货"); + } + if (arrival.getArrivalQuantity() == null || arrival.getArrivalQuantity().signum() <= 0) + { + throw new ServiceException("到货数量必须大于0"); + } + if (arrival.getArrivalDate() == null) + { + throw new ServiceException("到货日期不能为空"); + } + BigDecimal activeQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(request.getId()); + if (activeQuantity.add(arrival.getArrivalQuantity()).compareTo(request.getQuantity()) > 0) + { + throw new ServiceException("本次到货数量超过剩余可登记数量"); + } + arrival.setArrivalNo(generateArrivalNo()); + arrival.setAcceptanceStatus(0); + arrival.setCreateBy(SecurityUtils.getUsername()); + arrival.setRemark(trimToNull(arrival.getRemark())); + return purchaseArrivalMapper.insertPurchaseArrival(arrival); + } + + @Override + @Transactional + public int acceptPurchaseArrival(Long id, PurchaseArrivalAcceptanceRequest acceptance) + { + PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalByIdForUpdate(id); + if (arrival == null) + { + throw new ServiceException("到货记录不存在"); + } + requireOwnerOrManager(arrival.getApplicantId()); + if (!Integer.valueOf(0).equals(arrival.getAcceptanceStatus())) + { + throw new ServiceException("该到货记录已验收,不能重复操作"); + } + if (acceptance == null || acceptance.getAcceptanceStatus() == null + || (acceptance.getAcceptanceStatus() != 1 && acceptance.getAcceptanceStatus() != 2)) + { + throw new ServiceException("验收结果不正确"); + } + + String username = SecurityUtils.getUsername(); + PurchaseRequest request = null; + if (Integer.valueOf(1).equals(acceptance.getAcceptanceStatus())) + { + request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId()); + arrival.setPreAcceptPurchaseStatus(request.getPurchaseStatus()); + } + arrival.setAcceptanceStatus(acceptance.getAcceptanceStatus()); + arrival.setAcceptanceRemark(trimToNull(acceptance.getAcceptanceRemark())); + arrival.setAcceptedBy(username); + arrival.setAcceptedTime(new Date()); + arrival.setUpdateBy(username); + int rows = purchaseArrivalMapper.updatePurchaseArrivalAcceptance(arrival); + + if (Integer.valueOf(1).equals(arrival.getAcceptanceStatus())) + { + createPendingInRecord(arrival, username); + BigDecimal accepted = purchaseArrivalMapper.selectAcceptedQuantityByRequestId(arrival.getPurchaseRequestId()); + int status = accepted.compareTo(request.getQuantity()) >= 0 ? 4 : 3; + purchaseRequestMapper.updatePurchaseStatus(request.getId(), status, + arrival.getAcceptanceRemark(), username); + } + return rows; + } + + @Override + @Transactional + public int revokePurchaseArrivalAcceptance(Long id, PurchaseArrivalRevokeRequest revoke) + { + PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalByIdForUpdate(id); + if (arrival == null) + { + throw new ServiceException("到货记录不存在"); + } + requireOwnerOrManager(arrival.getApplicantId()); + if (Integer.valueOf(0).equals(arrival.getAcceptanceStatus())) + { + throw new ServiceException("该到货记录尚未验收,无需撤销"); + } + if (revoke == null || StringUtils.isBlank(revoke.getRevokeRemark())) + { + throw new ServiceException("撤销原因不能为空"); + } + + PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId()); + if (Integer.valueOf(2).equals(arrival.getAcceptanceStatus())) + { + if (Integer.valueOf(5).equals(request.getPurchaseStatus())) + { + throw new ServiceException("采购申请已取消,不能撤销拒收"); + } + BigDecimal activeQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(request.getId()); + if (activeQuantity.add(arrival.getArrivalQuantity()).compareTo(request.getQuantity()) > 0) + { + throw new ServiceException("拒收后已登记替代到货,撤销会超过采购数量,请先删除对应的待验收到货"); + } + } + else + { + InRecord inRecord = inRecordMapper.selectInRecordByArrivalIdForUpdate(id); + if (inRecord == null) + { + throw new ServiceException("未找到验收生成的待入库记录,不能自动撤销"); + } + if (Integer.valueOf(1).equals(inRecord.getInStatus())) + { + throw new ServiceException("该批到货已经确认入库,不能直接撤销,请先办理库存冲销"); + } + if (inRecordMapper.deletePendingInRecordByArrivalId(id) != 1) + { + throw new ServiceException("待入库记录状态已变化,请刷新后重试"); + } + } + + String username = SecurityUtils.getUsername(); + arrival.setRevokedBy(username); + arrival.setRevokedTime(new Date()); + arrival.setRevokeRemark(revoke.getRevokeRemark().trim()); + arrival.setUpdateBy(username); + int rows = purchaseArrivalMapper.revokePurchaseArrivalAcceptance(arrival); + if (rows != 1) + { + throw new ServiceException("到货验收状态已变化,请刷新后重试"); + } + + if (Integer.valueOf(1).equals(arrival.getAcceptanceStatus())) + { + BigDecimal accepted = purchaseArrivalMapper.selectAcceptedQuantityByRequestId(request.getId()); + int status; + if (accepted.signum() == 0) + { + Integer previousStatus = arrival.getPreAcceptPurchaseStatus(); + status = previousStatus != null && previousStatus >= 0 && previousStatus <= 2 ? previousStatus : 2; + } + else + { + status = accepted.compareTo(request.getQuantity()) >= 0 ? 4 : 3; + } + purchaseRequestMapper.updatePurchaseStatus(request.getId(), status, + "撤销到货验收:" + arrival.getRevokeRemark(), username); + } + return rows; + } + + @Override + @Transactional + public int deletePurchaseArrival(Long id) + { + PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalById(id); + if (arrival == null) + { + throw new ServiceException("到货记录不存在"); + } + requireOwnerOrManager(arrival.getApplicantId()); + if (!Integer.valueOf(0).equals(arrival.getAcceptanceStatus())) + { + throw new ServiceException("已验收的到货记录不能删除"); + } + return purchaseArrivalMapper.deletePendingPurchaseArrival(id); + } + + private void createPendingInRecord(PurchaseArrival arrival, String username) + { + InRecord inRecord = new InRecord(); + inRecord.setInNo("AIN" + arrival.getId()); + inRecord.setAreaId(null); + inRecord.setProductId(arrival.getProductId()); + inRecord.setPurchaseRequestId(arrival.getPurchaseRequestId()); + inRecord.setArrivalId(arrival.getId()); + inRecord.setSourceNo(arrival.getArrivalNo()); + inRecord.setInType(2); + inRecord.setInNum(arrival.getArrivalQuantity()); + inRecord.setInStatus(0); + inRecord.setOperator(username); + inRecord.setInOperator(null); + inRecord.setRemark("采购到货验收:" + arrival.getRequestNo()); + inRecordMapper.insertInRecord(inRecord); + } + + private void requireOwnerOrManager(PurchaseRequest request) + { + if (request == null) + { + throw new ServiceException("采购申请不存在"); + } + requireOwnerOrManager(request.getApplicantId()); + } + + private void requireOwnerOrManager(Long applicantId) + { + if (!isManager() && !SecurityUtils.getUserId().equals(applicantId)) + { + throw new ServiceException("只能处理自己的采购到货"); + } + } + + private boolean isManager() + { + return SecurityUtils.isAdmin() || SecurityUtils.hasPermi("warehouse:purchase:manage"); + } + + private String trimToNull(String value) + { + return StringUtils.isBlank(value) ? null : value.trim(); + } + + private String generateArrivalNo() + { + int sequence = ARRIVAL_SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1); + return "PA" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + + String.format("%03d", sequence); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseBatchServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseBatchServiceImpl.java new file mode 100644 index 0000000..09b069b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseBatchServiceImpl.java @@ -0,0 +1,141 @@ +package com.ruoyi.warehouse.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.warehouse.domain.PurchaseBatch; +import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest; +import com.ruoyi.warehouse.mapper.PurchaseBatchMapper; +import com.ruoyi.warehouse.service.IPurchaseBatchService; + +@Service +public class PurchaseBatchServiceImpl implements IPurchaseBatchService +{ + @Autowired + private PurchaseBatchMapper purchaseBatchMapper; + + @Override + public List selectPurchaseBatchList(PurchaseBatch batch) + { + return purchaseBatchMapper.selectPurchaseBatchList(batch); + } + + @Override + public List selectPurchaseBatchOptions(String keyword) + { + return purchaseBatchMapper.selectPurchaseBatchOptions(StringUtils.trim(keyword)); + } + + @Override + public List selectPurchaseBatchFilterOptions(String keyword) + { + return purchaseBatchMapper.selectPurchaseBatchFilterOptions(StringUtils.trim(keyword)); + } + + @Override + public int insertPurchaseBatch(PurchaseBatch batch) + { + checkManager(); + if (batch.getPurchaseStatus() == null) + { + batch.setPurchaseStatus(1); + } + normalizeAndValidate(batch); + if (purchaseBatchMapper.selectPurchaseBatchByNo(batch.getBatchNo()) != null) + { + throw new ServiceException("采购批次已存在"); + } + batch.setCreateBy(SecurityUtils.getUsername()); + return purchaseBatchMapper.insertPurchaseBatch(batch); + } + + @Override + @Transactional + public int updatePurchaseBatch(PurchaseBatch batch) + { + checkManager(); + if (batch.getId() == null || purchaseBatchMapper.selectPurchaseBatchById(batch.getId()) == null) + { + throw new ServiceException("采购批次不存在"); + } + normalizeAndValidate(batch); + PurchaseBatch sameNo = purchaseBatchMapper.selectPurchaseBatchByNo(batch.getBatchNo()); + if (sameNo != null && !sameNo.getId().equals(batch.getId())) + { + throw new ServiceException("采购批次已存在"); + } + String username = SecurityUtils.getUsername(); + batch.setUpdateBy(username); + int rows = purchaseBatchMapper.updatePurchaseBatch(batch); + purchaseBatchMapper.syncRequestBatchNo(batch.getId(), batch.getBatchNo(), username); + return rows; + } + + @Override + @Transactional + public int assignPurchaseBatch(PurchaseBatchAssignRequest request) + { + checkManager(); + if (request == null || request.getRequestIds() == null || request.getRequestIds().length == 0) + { + throw new ServiceException("请选择要分配或延期的采购申请"); + } + if (request.getBatchId() == null) + { + throw new ServiceException("请选择目标采购批次"); + } + PurchaseBatch batch = purchaseBatchMapper.selectPurchaseBatchById(request.getBatchId()); + if (batch == null) + { + throw new ServiceException("目标采购批次不存在"); + } + if (purchaseBatchMapper.countRequestsByIds(request.getRequestIds()) != request.getRequestIds().length) + { + throw new ServiceException("部分采购申请不存在,批次分配失败"); + } + purchaseBatchMapper.assignRequests(request.getRequestIds(), batch, SecurityUtils.getUsername()); + return request.getRequestIds().length; + } + + @Override + public int deletePurchaseBatchById(Long id) + { + checkManager(); + if (purchaseBatchMapper.selectPurchaseBatchById(id) == null) + { + throw new ServiceException("采购批次不存在"); + } + if (purchaseBatchMapper.countRequestsByBatchId(id) > 0) + { + throw new ServiceException("该批次仍有关联的采购申请,不能删除"); + } + return purchaseBatchMapper.deletePurchaseBatchById(id); + } + + private void normalizeAndValidate(PurchaseBatch batch) + { + batch.setBatchNo(StringUtils.trim(batch.getBatchNo())); + batch.setBatchName(StringUtils.trim(batch.getBatchName())); + batch.setProgressRemark(StringUtils.trim(batch.getProgressRemark())); + if (StringUtils.isBlank(batch.getBatchNo())) + { + throw new ServiceException("采购批次编号不能为空"); + } + if (batch.getPurchaseStatus() == null || batch.getPurchaseStatus() < 1 || batch.getPurchaseStatus() > 5) + { + throw new ServiceException("采购批次状态不正确"); + } + } + + private void checkManager() + { + if (!SecurityUtils.isAdmin() && !SecurityUtils.hasPermi("warehouse:purchase:manage")) + { + throw new ServiceException("只有管理员可以维护采购批次"); + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseRequestServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseRequestServiceImpl.java new file mode 100644 index 0000000..06660b7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseRequestServiceImpl.java @@ -0,0 +1,411 @@ +package com.ruoyi.warehouse.service.impl; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.warehouse.domain.ProductInfo; +import com.ruoyi.warehouse.domain.PurchaseRequest; +import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest; +import com.ruoyi.warehouse.mapper.ProductInfoMapper; +import com.ruoyi.warehouse.mapper.PurchaseArrivalMapper; +import com.ruoyi.warehouse.mapper.PurchaseRequestMapper; +import com.ruoyi.warehouse.service.IProductInfoService; +import com.ruoyi.warehouse.service.IPurchaseRequestService; + +@Service +public class PurchaseRequestServiceImpl implements IPurchaseRequestService +{ + private static final AtomicInteger REQUEST_SEQUENCE = new AtomicInteger(); + @Autowired + private PurchaseRequestMapper purchaseRequestMapper; + + @Autowired + private ProductInfoMapper productInfoMapper; + + @Autowired + private IProductInfoService productInfoService; + + @Autowired + private PurchaseArrivalMapper purchaseArrivalMapper; + + @Override + public List selectPurchaseRequestList(PurchaseRequest purchaseRequest) + { + return purchaseRequestMapper.selectPurchaseRequestList(purchaseRequest); + } + + @Override + public PurchaseRequest selectPurchaseRequestById(Long id) + { + return requireRequest(id); + } + + @Override + @Transactional + public int insertPurchaseRequest(PurchaseRequest purchaseRequest) + { + normalize(purchaseRequest); + validateRequest(purchaseRequest); + purchaseRequest.setApplicantId(SecurityUtils.getUserId()); + purchaseRequest.setApplicantName(SecurityUtils.getUsername()); + purchaseRequest.setCreateBy(SecurityUtils.getUsername()); + purchaseRequest.setPurchaseStatus(0); + purchaseRequest.setPurchaseBatchNo(null); + purchaseRequest.setProgressRemark(null); + if (StringUtils.isBlank(purchaseRequest.getRequestNo())) + { + purchaseRequest.setRequestNo(generateRequestNo()); + } + else if (purchaseRequestMapper.checkRequestNoUnique(purchaseRequest.getRequestNo()) != null) + { + throw new ServiceException("申请单号已存在"); + } + resolveProduct(purchaseRequest); + return purchaseRequestMapper.insertPurchaseRequest(purchaseRequest); + } + + @Override + @Transactional + public int updatePurchaseRequest(PurchaseRequest purchaseRequest) + { + if (purchaseRequest.getId() == null) + { + throw new ServiceException("采购申请ID不能为空"); + } + PurchaseRequest existing = requireRequest(purchaseRequest.getId()); + checkOwnerOrManager(existing); + normalize(purchaseRequest); + validateRequest(purchaseRequest); + + BigDecimal activeArrivalQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(existing.getId()); + if (activeArrivalQuantity.compareTo(purchaseRequest.getQuantity()) > 0) + { + throw new ServiceException("需求数量不能小于已登记或已验收的到货数量"); + } + if (activeArrivalQuantity.signum() > 0 && !sameMaterial(existing, purchaseRequest)) + { + throw new ServiceException("已有到货记录,不能更换采购物料"); + } + + if (StringUtils.isBlank(purchaseRequest.getRequestNo())) + { + purchaseRequest.setRequestNo(existing.getRequestNo()); + } + + PurchaseRequest sameNo = purchaseRequestMapper.checkRequestNoUnique(purchaseRequest.getRequestNo()); + if (sameNo != null && !sameNo.getId().equals(purchaseRequest.getId())) + { + throw new ServiceException("申请单号已存在"); + } + + // 申请归属和采购进度只能由系统及进度管理接口维护。 + purchaseRequest.setApplicantId(existing.getApplicantId()); + purchaseRequest.setApplicantName(existing.getApplicantName()); + purchaseRequest.setPurchaseStatus(existing.getPurchaseStatus()); + purchaseRequest.setPurchaseBatchNo(existing.getPurchaseBatchNo()); + purchaseRequest.setProgressRemark(existing.getProgressRemark()); + purchaseRequest.setUpdateBy(SecurityUtils.getUsername()); + resolveProduct(purchaseRequest); + int rows = purchaseRequestMapper.updatePurchaseRequest(purchaseRequest, currentApplicantScope()); + if (rows == 0) + { + throw new ServiceException("无权修改其他员工的采购申请"); + } + return rows; + } + + @Override + @Transactional + public int deletePurchaseRequestByIds(Long[] ids) + { + if (ids == null || ids.length == 0) + { + throw new ServiceException("请选择采购申请"); + } + for (Long id : ids) + { + PurchaseRequest request = requireRequest(id); + checkOwnerOrManager(request); + if (purchaseArrivalMapper.countByRequestId(id) > 0) + { + throw new ServiceException("申请 " + request.getRequestNo() + " 已有到货记录,不能删除"); + } + } + int rows = purchaseRequestMapper.deletePurchaseRequestByIds(ids, currentApplicantScope()); + if (rows != ids.length) + { + throw new ServiceException("只能删除自己的采购申请"); + } + return rows; + } + + @Override + @Transactional + public int updatePurchaseStatus(Long id, PurchaseStatusUpdateRequest request) + { + PurchaseRequest existing = requireRequest(id); + checkOwnerOrManager(existing); + Integer status = request.getPurchaseStatus(); + if (status == null || (status != 0 && status != 1 && status != 2 && status != 5)) + { + throw new ServiceException("到货相关状态由验收数量自动计算"); + } + if (purchaseArrivalMapper.selectAcceptedQuantityByRequestId(id).signum() > 0) + { + throw new ServiceException("已有验收到货,采购状态由验收进度自动计算"); + } + if (status == 5 && purchaseArrivalMapper.selectActiveQuantityByRequestId(id).signum() > 0) + { + throw new ServiceException("已有待验收或已验收到货,不能取消采购申请"); + } + return purchaseRequestMapper.updatePurchaseStatus(id, status, + trimToNull(request.getProgressRemark()), SecurityUtils.getUsername()); + } + + @Override + @Transactional + public int batchInsertPurchaseRequest(List requestList) + { + if (requestList == null || requestList.isEmpty()) + { + throw new ServiceException("表格数据不能为空"); + } + int count = 0; + for (int index = 0; index < requestList.size(); index++) + { + try + { + insertPurchaseRequest(requestList.get(index)); + count++; + } + catch (Exception e) + { + throw new ServiceException("第" + (index + 1) + "行:" + e.getMessage()); + } + } + return count; + } + + @Override + @Transactional + public String importPurchaseRequest(List requestList, String username) + { + if (requestList == null || requestList.isEmpty()) + { + throw new ServiceException("导入数据不能为空"); + } + int successCount = 0; + StringBuilder failure = new StringBuilder(); + for (int index = 0; index < requestList.size(); index++) + { + try + { + PurchaseRequest request = requestList.get(index); + request.setApplicantId(SecurityUtils.getUserId()); + request.setApplicantName(username); + request.setCreateBy(username); + insertPurchaseRequest(request); + successCount++; + } + catch (Exception e) + { + failure.append("第").append(index + 2).append("行:").append(e.getMessage()).append(";"); + } + } + if (failure.length() > 0) + { + throw new ServiceException("导入失败," + failure); + } + return "成功导入 " + successCount + " 条采购申请"; + } + + private PurchaseRequest requireRequest(Long id) + { + PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestById(id, null); + if (request == null) + { + throw new ServiceException("采购申请不存在"); + } + return request; + } + + private void checkOwnerOrManager(PurchaseRequest request) + { + if (!isPurchaseManager() && !SecurityUtils.getUserId().equals(request.getApplicantId())) + { + throw new ServiceException("无权操作其他员工的采购申请"); + } + } + + private boolean isPurchaseManager() + { + return SecurityUtils.isAdmin() || SecurityUtils.hasPermi("warehouse:purchase:manage"); + } + + private Long currentApplicantScope() + { + return isPurchaseManager() ? null : SecurityUtils.getUserId(); + } + + private void normalize(PurchaseRequest request) + { + request.setRequestNo(StringUtils.trim(request.getRequestNo())); + request.setProductCode(trimToNull(request.getProductCode())); + request.setMaterialName(StringUtils.trim(request.getMaterialName())); + request.setMaterialBrand(trimToNull(request.getMaterialBrand())); + request.setCategory(StringUtils.trim(request.getCategory())); + request.setMaterialParams(StringUtils.trim(request.getMaterialParams())); + request.setPurchaseLink(StringUtils.trim(request.getPurchaseLink())); + request.setUnit(StringUtils.trim(request.getUnit())); + request.setRequestRemark(trimToNull(request.getRequestRemark())); + if (StringUtils.isBlank(request.getRemark()) && StringUtils.isNotBlank(request.getRequestRemark())) + { + request.setRemark(request.getRequestRemark()); + } + } + + private void validateRequest(PurchaseRequest request) + { + if (StringUtils.isBlank(request.getMaterialName())) + { + throw new ServiceException("物料名称不能为空"); + } + if (request.getQuantity() == null || request.getQuantity().signum() <= 0) + { + throw new ServiceException("需求数量必须大于0"); + } + if (StringUtils.isNotBlank(request.getPurchaseLink()) + && !request.getPurchaseLink().matches("(?i)^https?://.+")) + { + throw new ServiceException("采购链接必须以 http:// 或 https:// 开头"); + } + } + + private void resolveProduct(PurchaseRequest request) + { + ProductInfo draft = new ProductInfo(); + draft.setProductCode(request.getProductCode()); + draft.setProductName(request.getMaterialName()); + draft.setBrand(request.getMaterialBrand()); + draft.setCategory(trimToNull(request.getCategory())); + draft.setSpec(trimToNull(request.getMaterialParams())); + draft.setUnit(trimToNull(request.getUnit())); + draft.setRemark(trimToNull(request.getProductRemark())); + + ProductInfo product = null; + if (request.getProductId() != null) + { + ProductInfo selected = productInfoMapper.selectProductInfoById(request.getProductId()); + if (selected == null) + { + throw new ServiceException("选择的产品信息不存在"); + } + if (sameProduct(draft, selected)) + { + product = selected; + } + } + + if (product == null) + { + product = resolveProductByCode(draft); + } + if (product == null && StringUtils.isBlank(draft.getCategory())) + { + draft.setCategory(productInfoService.inferProductCategory(draft)); + request.setCategory(draft.getCategory()); + } + if (product == null) + { + product = productInfoMapper.checkProductInfoUnique(draft); + } + if (product == null) + { + productInfoService.insertProductInfo(draft); + product = draft; + } + + request.setProductId(product.getId()); + request.setProductCode(product.getProductCode()); + request.setMaterialName(product.getProductName()); + request.setMaterialBrand(product.getBrand()); + request.setCategory(product.getCategory()); + request.setMaterialParams(product.getSpec()); + request.setUnit(product.getUnit()); + request.setProductRemark(product.getRemark()); + } + + private ProductInfo resolveProductByCode(ProductInfo draft) + { + if (StringUtils.isBlank(draft.getProductCode())) + { + return null; + } + ProductInfo product = productInfoMapper.checkProductCodeUnique(draft.getProductCode()); + if (product == null) + { + return null; + } + if (!matchesProvidedField(draft.getProductName(), product.getProductName()) + || !matchesProvidedField(draft.getBrand(), product.getBrand()) + || !matchesProvidedField(draft.getCategory(), product.getCategory()) + || !matchesProvidedField(draft.getSpec(), product.getSpec()) + || !matchesProvidedField(draft.getUnit(), product.getUnit())) + { + throw new ServiceException("物料编码 " + draft.getProductCode() + " 与导入的物料信息不一致"); + } + return product; + } + + private boolean matchesProvidedField(String imported, String existing) + { + return StringUtils.isBlank(imported) || sameText(imported, existing); + } + + private boolean sameProduct(ProductInfo left, ProductInfo right) + { + return sameText(left.getProductName(), right.getProductName()) + && sameText(left.getBrand(), right.getBrand()) + && sameText(left.getCategory(), right.getCategory()) + && sameText(left.getSpec(), right.getSpec()); + } + + private boolean sameMaterial(PurchaseRequest left, PurchaseRequest right) + { + return sameText(left.getMaterialName(), right.getMaterialName()) + && sameText(left.getMaterialBrand(), right.getMaterialBrand()) + && sameText(left.getCategory(), right.getCategory()) + && sameText(left.getMaterialParams(), right.getMaterialParams()); + } + + private boolean sameText(String left, String right) + { + String leftValue = trimToNull(left); + String rightValue = trimToNull(right); + if (leftValue == null || rightValue == null) + { + return leftValue == rightValue; + } + return leftValue.equalsIgnoreCase(rightValue); + } + + private String trimToNull(String value) + { + return StringUtils.isBlank(value) ? null : value.trim(); + } + + private String generateRequestNo() + { + int sequence = REQUEST_SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1); + return "PR" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + + String.format("%03d", sequence); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseSmartImportServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseSmartImportServiceImpl.java new file mode 100644 index 0000000..1a91920 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/PurchaseSmartImportServiceImpl.java @@ -0,0 +1,756 @@ +package com.ruoyi.warehouse.service.impl; + +import java.io.InputStream; +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.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.apache.commons.io.FilenameUtils; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.warehouse.domain.ProductInfo; +import com.ruoyi.warehouse.service.IProductInfoService; +import com.ruoyi.warehouse.service.IPurchaseSmartImportService; + +@Service +public class PurchaseSmartImportServiceImpl implements IPurchaseSmartImportService +{ + private static final int MAX_ROWS = 100; + private static final int REVIEW_CHUNK_SIZE = 8; + private static final long MAX_FILE_SIZE = 10L * 1024 * 1024; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .version(HttpClient.Version.HTTP_1_1) + .build(); + + @Value("${agent.model.base-url}") + private String textModelBaseUrl; + + @Value("${agent.model.api-key:}") + private String textModelApiKey; + + @Value("${agent.model.model}") + private String textModel; + + @Autowired + private IProductInfoService productService; + + @Override + public JSONObject analyze(MultipartFile file) throws Exception + { + validateFile(file); + String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT); + List products = productService.selectProductInfoAll(); + String output = isExcel(extension) ? analyzeExcel(file, products) : analyzeImage(file, extension, products); + JSONObject result = normalizeResult(output, products); + if (result.getJSONArray("items").isEmpty()) + { + throw new ServiceException("没有识别到可导入的采购明细"); + } + return result; + } + + private String analyzeExcel(MultipartFile file, List products) throws Exception + { + JSONArray rows = readExcel(file); + if (rows.isEmpty()) + { + throw new ServiceException("Excel中没有可分析的数据"); + } + JSONObject mapping = resolveHeaderMapping(rows); + JSONArray items = new JSONArray(); + for (int i = 0; i < rows.size(); i++) + { + JSONObject source = rows.getJSONObject(i); + JSONObject item = new JSONObject(); + for (String header : mapping.keySet()) + { + String field = mapping.getString(header); + Object value = source.get(header); + if (StringUtils.isNotBlank(field) && value != null && StringUtils.isNotBlank(value.toString())) + { + item.put(field, value); + } + } + if (!item.isEmpty()) items.add(item); + } + reviewExcelItems(items, products); + propagateBrands(items, products); + return JSONObject.of("items", items).toJSONString(); + } + + private void propagateBrands(JSONArray items, List products) + { + List brands = new ArrayList<>(); + for (ProductInfo product : products) addDistinctBrand(brands, product.getBrand()); + for (int i = 0; i < items.size(); i++) addDistinctBrand(brands, items.getJSONObject(i).getString("materialBrand")); + brands.sort(Comparator.comparingInt(String::length).reversed()); + + for (int i = 0; i < items.size(); i++) + { + JSONObject item = items.getJSONObject(i); + if (StringUtils.isNotBlank(item.getString("materialBrand"))) continue; + String spec = normalizeSearchText(item.getString("materialParams")); + if (StringUtils.isBlank(spec)) continue; + for (String brand : brands) + { + if (!spec.startsWith(normalizeSearchText(brand))) continue; + item.put("materialBrand", brand); + JSONArray inferred = item.getJSONArray("inferredFields"); + if (inferred == null) + { + inferred = new JSONArray(); + item.put("inferredFields", inferred); + } + if (!inferred.contains("materialBrand")) inferred.add("materialBrand"); + break; + } + } + } + + private void addDistinctBrand(List brands, String brand) + { + String value = StringUtils.trim(brand); + if (StringUtils.isBlank(value)) return; + for (String existing : brands) + { + if (same(existing, value)) return; + } + brands.add(value); + } + + private void reviewExcelItems(JSONArray items, List products) throws Exception + { + JSONArray existingCategories = existingCategories(products); + List> tasks = new ArrayList<>(); + for (int start = 0; start < items.size(); start += REVIEW_CHUNK_SIZE) + { + JSONArray rows = new JSONArray(); + for (int i = start; i < Math.min(start + REVIEW_CHUNK_SIZE, items.size()); i++) + { + JSONObject row = new JSONObject(items.getJSONObject(i)); + row.put("_rowIndex", i); + rows.add(row); + } + String prompt = "逐行检查采购数据内容。识别单元格内容错位、名称和型号混在一起、数量带单位、日期格式不规范等问题," + + "并补全品牌、类别、单位。每一行的category都不能为空,优先从existingCategories选择最合适分类;" + + "无法精确判断时选择最接近的通用分类。不要改变正确内容,不要臆造品牌或型号。" + + "quantity只能来自原数量列;规格中的包装数量(如【600个】、每包500个、1000PCS)属于materialParams," + + "绝不能作为采购quantity。原quantity是有效数字时禁止修改,原数量缺失时也不要从规格推断。" + + "使用紧凑JSON且不要格式化换行,只返回需要修改或补全的字段,格式:" + + "{\"patches\":[{\"rowIndex\":0,\"fields\":{\"字段\":\"新值\"},\"warning\":\"仅有歧义时说明\"}]}。" + + "常规补全不要返回warning或inferredFields。允许字段仅为productCode,materialName,materialBrand," + + "category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark。无修改返回空patches。rows=" + + rows.toJSONString() + ",existingCategories=" + existingCategories.toJSONString(); + tasks.add(CompletableFuture.supplyAsync(() -> { + try + { + return callInventoryModel(JSONArray.of( + message("system", "你是采购Excel数据校验器,只返回JSON对象。"), message("user", prompt)), 2048); + } + catch (Exception e) + { + throw new CompletionException(e); + } + })); + } + + try + { + for (CompletableFuture task : tasks) + { + JSONObject result = JSON.parseObject(extractJson(task.join())); + JSONArray patches = result.getJSONArray("patches"); + if (patches == null) continue; + for (int i = 0; i < patches.size(); i++) applyReviewPatch(items, patches.getJSONObject(i)); + } + } + catch (CompletionException e) + { + Throwable cause = e.getCause(); + if (cause instanceof ServiceException) throw (ServiceException) cause; + throw new ServiceException("库存AI校验超时或失败,请稍后重试"); + } + } + + private void applyReviewPatch(JSONArray items, JSONObject patch) + { + if (patch == null) return; + Integer rowIndex = patch.getInteger("rowIndex"); + if (rowIndex == null || rowIndex < 0 || rowIndex >= items.size()) return; + JSONObject item = items.getJSONObject(rowIndex); + JSONObject fields = patch.getJSONObject("fields"); + if (fields != null) + { + JSONArray inferred = new JSONArray(); + for (String field : fields.keySet()) + { + if (isImportField(field)) + { + if ("quantity".equals(field) && !canApplyQuantityPatch(item.get("quantity"))) continue; + item.put(field, fields.get(field)); + inferred.add(field); + } + } + if (!inferred.isEmpty()) item.put("inferredFields", inferred); + } + if (StringUtils.isNotBlank(patch.getString("warning"))) item.put("warning", patch.getString("warning")); + } + + private boolean canApplyQuantityPatch(Object originalQuantity) + { + if (originalQuantity == null || StringUtils.isBlank(originalQuantity.toString())) return false; + String value = originalQuantity.toString().replace(",", "").trim(); + return !value.matches("^\\d+(?:\\.\\d+)?$"); + } + + private JSONArray existingCategories(List products) + { + JSONArray categories = new JSONArray(); + for (ProductInfo product : products) + { + String category = StringUtils.trim(product.getCategory()); + if (StringUtils.isNotBlank(category) && !categories.contains(category)) categories.add(category); + if (categories.size() >= 100) break; + } + return categories; + } + + private JSONObject resolveHeaderMapping(JSONArray rows) throws Exception + { + JSONObject mapping = new JSONObject(); + JSONObject first = rows.getJSONObject(0); + JSONArray unknownHeaders = new JSONArray(); + for (String header : first.keySet()) + { + if ("_sourceRow".equals(header)) continue; + String field = knownHeaderField(header); + if (field == null) + { + if (!isIgnoredHeader(header)) unknownHeaders.add(header); + } + else mapping.put(header, field); + } + + if (!mapping.containsValue("materialName") || !unknownHeaders.isEmpty()) + { + JSONArray samples = new JSONArray(); + for (int i = 0; i < Math.min(3, rows.size()); i++) samples.add(rows.get(i)); + String prompt = "只分析Excel表头映射,不处理每行数据。可用目标字段为productCode,materialName," + + "materialBrand,category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark。" + + "单价、总价、申请人等无关列不要映射。只返回JSON:{\"mapping\":{\"原表头\":\"目标字段\"}}。" + + "headers=" + first.keySet() + ", samples=" + samples.toJSONString(); + String output = callInventoryModel(JSONArray.of(message("system", "你是Excel表头映射器,只返回JSON对象。"), + message("user", prompt)), 512); + JSONObject aiMapping = JSON.parseObject(extractJson(output)).getJSONObject("mapping"); + if (aiMapping != null) + { + for (String header : aiMapping.keySet()) + { + String field = aiMapping.getString(header); + if (first.containsKey(header) && isImportField(field)) mapping.put(header, field); + } + } + } + if (!mapping.containsValue("materialName")) + { + throw new ServiceException("AI未能识别物料名称列,请检查Excel表头"); + } + return mapping; + } + + private String analyzeImage(MultipartFile file, String extension, List products) throws Exception + { + String mime = "jpg".equals(extension) || "jpeg".equals(extension) ? "image/jpeg" : "image/" + extension; + JSONArray content = new JSONArray(); + content.add(JSONObject.of("type", "text", "text", + "识别图片中的采购表格,并参考 existingProducts 关联物料或补全缺失信息。existingProducts=" + + productCatalog(products).toJSONString())); + content.add(JSONObject.of("type", "image_url", "image_url", JSONObject.of("url", + "data:" + mime + ";base64," + Base64.getEncoder().encodeToString(file.getBytes())))); + return callInventoryModel(JSONArray.of(message("system", systemPrompt()), message("user", content))); + } + + private JSONArray readExcel(MultipartFile file) throws Exception + { + JSONArray result = new JSONArray(); + try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) + { + DataFormatter formatter = new DataFormatter(Locale.CHINA); + FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); + HeaderLocation location = findHeader(workbook, formatter, evaluator); + if (location == null) + { + throw new ServiceException("未识别到Excel表头,请确认表头包含物料名称、品名或产品名称"); + } + Sheet sheet = location.sheet; + int headerIndex = location.rowIndex; + Row header = sheet.getRow(headerIndex); + int lastColumn = lastHeaderColumn(sheet, headerIndex, header); + for (int rowIndex = headerIndex + 1; rowIndex <= sheet.getLastRowNum() && result.size() < MAX_ROWS; rowIndex++) + { + Row row = sheet.getRow(rowIndex); + JSONObject values = new JSONObject(); + if (row != null) + { + for (int column = 0; column < lastColumn; column++) + { + String name = headerCellText(sheet, headerIndex, column, formatter, evaluator); + String value = dataCellText(sheet, rowIndex, column, formatter, evaluator); + if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value)) + { + values.put(name, value); + } + } + } + if (!values.isEmpty()) + { + values.put("_sourceRow", rowIndex + 1); + result.add(values); + } + } + } + return result; + } + + private HeaderLocation findHeader(Workbook workbook, DataFormatter formatter, FormulaEvaluator evaluator) + { + HeaderLocation best = null; + int bestScore = 0; + HeaderLocation fallback = null; + int fallbackScore = 0; + for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) + { + Sheet sheet = workbook.getSheetAt(sheetIndex); + int end = Math.min(sheet.getLastRowNum(), sheet.getFirstRowNum() + 50); + for (int i = sheet.getFirstRowNum(); i <= end; i++) + { + Row row = sheet.getRow(i); + int nonEmpty = 0; + int recognized = 0; + boolean hasNameHeader = false; + int rowLastColumn = lastHeaderColumn(sheet, i, row); + if (rowLastColumn > 0) + { + for (int column = 0; column < rowLastColumn; column++) + { + String value = headerCellText(sheet, i, column, formatter, evaluator).replaceAll("\\s+", "") + .toLowerCase(Locale.ROOT); + if (StringUtils.isBlank(value)) continue; + nonEmpty++; + hasNameHeader |= value.contains("物料名称") || value.contains("产品名称") + || value.contains("物品名称") || value.equals("物品") || value.equals("物料") + || value.equals("产品") || value.equals("品名") || value.equals("名称") + || value.equals("materialname") || value.equals("productname") || value.equals("itemname"); + if (isKnownHeader(value)) recognized++; + } + } + int score = recognized * 10 + nonEmpty; + if ((hasNameHeader || recognized >= 3) && nonEmpty >= 2 + && (score > bestScore || score == bestScore && best != null && i > best.rowIndex)) + { + best = new HeaderLocation(sheet, i); + bestScore = score; + } + // AI负责最终字段映射。这里保留一个宽松候选,避免陌生表头在调用AI前被拦截。 + if (recognized >= 2 && nonEmpty >= 3 && score > fallbackScore) + { + fallback = new HeaderLocation(sheet, i); + fallbackScore = score; + } + } + } + return best != null ? best : fallback; + } + + private boolean isKnownHeader(String value) + { + return value.contains("物料") || value.contains("物品") || value.contains("产品") + || value.contains("品名") || value.equals("名称") + || value.contains("规格") || value.contains("型号") || value.contains("参数") + || value.contains("数量") || value.contains("品牌") || value.contains("类别") + || value.contains("分类") || value.contains("单位") || value.contains("链接") + || value.contains("日期") || value.contains("备注") || value.contains("material") + || value.contains("product") || value.contains("quantity") || value.contains("spec"); + } + + private static class HeaderLocation + { + private final Sheet sheet; + private final int rowIndex; + + private HeaderLocation(Sheet sheet, int rowIndex) + { + this.sheet = sheet; + this.rowIndex = rowIndex; + } + } + + private String cellText(Row row, int column, DataFormatter formatter, FormulaEvaluator evaluator) + { + if (row == null || row.getCell(column) == null) + { + return ""; + } + return StringUtils.trim(formatter.formatCellValue(row.getCell(column), evaluator)); + } + + private String headerCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter, + FormulaEvaluator evaluator) + { + return mergedCellText(sheet, rowIndex, column, formatter, evaluator, true); + } + + private String dataCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter, + FormulaEvaluator evaluator) + { + return mergedCellText(sheet, rowIndex, column, formatter, evaluator, false); + } + + private String mergedCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter, + FormulaEvaluator evaluator, boolean resolveHorizontal) + { + Row row = sheet.getRow(rowIndex); + String direct = cellText(row, column, formatter, evaluator); + if (StringUtils.isNotBlank(direct)) return direct; + for (int i = 0; i < sheet.getNumMergedRegions(); i++) + { + CellRangeAddress region = sheet.getMergedRegion(i); + if (!region.isInRange(rowIndex, column)) continue; + if (!resolveHorizontal && region.getFirstColumn() != region.getLastColumn()) return ""; + return cellText(sheet.getRow(region.getFirstRow()), region.getFirstColumn(), formatter, evaluator); + } + return ""; + } + + private int lastHeaderColumn(Sheet sheet, int headerIndex, Row header) + { + int lastColumn = header == null ? 0 : Math.max(0, header.getLastCellNum()); + for (int i = 0; i < sheet.getNumMergedRegions(); i++) + { + CellRangeAddress region = sheet.getMergedRegion(i); + if (headerIndex >= region.getFirstRow() && headerIndex <= region.getLastRow()) + { + lastColumn = Math.max(lastColumn, region.getLastColumn() + 1); + } + } + return lastColumn; + } + + private JSONArray productCatalog(List products) + { + JSONArray catalog = new JSONArray(); + for (ProductInfo product : products) + { + if (catalog.size() >= 300) + { + break; + } + catalog.add(JSONObject.of("productId", product.getId(), "productCode", product.getProductCode(), + "materialName", product.getProductName(), "materialBrand", product.getBrand(), + "category", product.getCategory(), "materialParams", product.getSpec(), "unit", product.getUnit())); + } + return catalog; + } + + private String knownHeaderField(String header) + { + String value = normalizeSearchText(header); + if (List.of("关联物料编码", "物料编码", "产品编码", "productcode", "materialcode").contains(value)) return "productCode"; + if (List.of("物料名称", "产品名称", "物品名称", "商品名称", "物品", "物料", "产品", "品名", "名称", + "materialname", "productname", "itemname").contains(value)) return "materialName"; + if (List.of("品牌", "厂牌", "brand").contains(value)) return "materialBrand"; + if (List.of("类别", "分类", "物料类别", "产品分类", "category").contains(value)) return "category"; + if (List.of("参数规格", "规格型号", "型号规格", "规格", "型号", "参数", "spec", "model").contains(value)) return "materialParams"; + if (List.of("单位", "计量单位", "unit").contains(value)) return "unit"; + if (List.of("需求数量", "采购数量", "数量", "qty", "quantity").contains(value)) return "quantity"; + if (List.of("期望到货日期", "期望到货", "预计到货日期", "到货日期", "expecteddate").contains(value)) return "expectedDate"; + if (List.of("采购链接", "参考链接", "商品链接", "链接", "url", "purchaselink").contains(value)) return "purchaseLink"; + if (List.of("产品备注", "物料备注", "productremark").contains(value)) return "productRemark"; + if (List.of("申请备注", "备注", "remark").contains(value)) return "remark"; + return null; + } + + private boolean isImportField(String field) + { + return List.of("productCode", "materialName", "materialBrand", "category", "materialParams", "unit", + "quantity", "expectedDate", "purchaseLink", "productRemark", "remark").contains(field); + } + + private boolean isIgnoredHeader(String header) + { + String value = normalizeSearchText(header); + return List.of("单价", "价格", "含税单价", "总价", "金额", "合计", "申请人", "申请者", "申请员工", + "序号", "行号", "编号", "部门", "税率", "供应商").contains(value); + } + + private String normalizeSearchText(String value) + { + return StringUtils.defaultString(value).replaceAll("[\\s_()()/\\-]+", "").toLowerCase(Locale.ROOT); + } + + private String callInventoryModel(JSONArray input, int maxTokens) throws Exception + { + return callInventoryModel(input, maxTokens, Duration.ofSeconds(120)); + } + + private String callInventoryModel(JSONArray input) throws Exception + { + return callInventoryModel(input, 8192, Duration.ofSeconds(120)); + } + + private String callInventoryModel(JSONArray input, int maxTokens, Duration timeout) throws Exception + { + JSONObject body = new JSONObject(); + body.put("model", textModel); + body.put("messages", input); + body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false)); + body.put("temperature", 0); + body.put("max_tokens", maxTokens); + body.put("stream", false); + HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(normalizeChatEndpoint(textModelBaseUrl))) + .timeout(timeout).header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8)); + if (StringUtils.isNotBlank(textModelApiKey)) + { + builder.header("Authorization", "Bearer " + textModelApiKey); + } + HttpResponse response = HTTP_CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) + { + throw new ServiceException("库存AI分析失败,HTTP " + response.statusCode()); + } + JSONObject root = JSON.parseObject(response.body()); + JSONArray choices = root.getJSONArray("choices"); + String text = choices == null || choices.isEmpty() ? null : choices.getJSONObject(0).getJSONObject("message").getString("content"); + if (StringUtils.isBlank(text)) + { + throw new ServiceException("库存AI未返回分析内容"); + } + return text; + } + + private JSONObject normalizeResult(String output, List products) + { + JSONObject root = JSON.parseObject(extractJson(output)); + JSONArray sourceItems = root.getJSONArray("items"); + JSONArray items = new JSONArray(); + List globalWarnings = new ArrayList<>(); + if (sourceItems == null) + { + return JSONObject.of("items", items, "warnings", JSONArray.of("模型返回格式不正确")); + } + for (int i = 0; i < sourceItems.size() && items.size() < MAX_ROWS; i++) + { + JSONObject source = sourceItems.getJSONObject(i); + if (source == null || StringUtils.isBlank(source.getString("materialName"))) + { + globalWarnings.add("第 " + (i + 1) + " 行未识别到物料名称,已跳过"); + continue; + } + JSONObject item = new JSONObject(); + copyFields(source, item); + ProductInfo matched = matchProduct(item, products); + if (matched != null) + { + item.put("productId", matched.getId()); + item.put("productCode", matched.getProductCode()); + item.put("materialName", matched.getProductName()); + item.put("materialBrand", matched.getBrand()); + item.put("category", matched.getCategory()); + item.put("materialParams", matched.getSpec()); + item.put("unit", matched.getUnit()); + String materialLabel = StringUtils.isNotBlank(matched.getProductCode()) + ? matched.getProductCode() : matched.getProductName() + " / " + matched.getSpec(); + appendTip(item, "已关联现有物料 " + materialLabel); + } + if (source.getJSONArray("inferredFields") != null && !source.getJSONArray("inferredFields").isEmpty()) + { + List labels = new ArrayList<>(); + for (String field : source.getJSONArray("inferredFields").toJavaList(String.class)) + { + String label = importFieldLabel(field); + if (!labels.contains(label)) labels.add(label); + } + appendTip(item, "AI推断字段:" + String.join("、", labels)); + } + if (StringUtils.isNotBlank(source.getString("warning"))) + { + appendTip(item, source.getString("warning")); + } + if (StringUtils.isBlank(item.getString("category"))) + { + String category = categoryByMaterialName(item.getString("materialName"), products); + if (StringUtils.isBlank(category)) category = defaultCategory(products); + item.put("category", category); + appendTip(item, "未能精确判断分类,已归入" + category); + } + items.add(item); + } + return JSONObject.of("items", items, "warnings", globalWarnings); + } + + private String categoryByMaterialName(String materialName, List products) + { + String category = null; + for (ProductInfo product : products) + { + if (!same(materialName, product.getProductName()) || StringUtils.isBlank(product.getCategory())) continue; + if (category != null && !same(category, product.getCategory())) return null; + category = product.getCategory(); + } + return category; + } + + private String defaultCategory(List products) + { + String[] preferred = { "其他", "通用物料", "其他物料", "未分类" }; + for (String candidate : preferred) + { + for (ProductInfo product : products) + { + if (same(candidate, product.getCategory())) return product.getCategory(); + } + } + return "其他"; + } + + private void copyFields(JSONObject source, JSONObject target) + { + String[] fields = { "productCode", "materialName", "materialBrand", "category", + "materialParams", "unit", "quantity", "expectedDate", "purchaseLink", "productRemark", "remark" }; + for (String field : fields) + { + target.put(field, source.get(field)); + } + if (target.getBigDecimal("quantity") == null || target.getBigDecimal("quantity").signum() <= 0) + { + target.put("quantity", BigDecimal.ONE); + appendTip(target, "未识别到有效数量,已暂填1"); + } + } + + private String importFieldLabel(String field) + { + if ("productCode".equals(field)) return "关联物料编码"; + if ("materialName".equals(field)) return "物料名称"; + if ("materialBrand".equals(field)) return "品牌"; + if ("category".equals(field)) return "分类"; + if ("materialParams".equals(field)) return "参数规格"; + if ("unit".equals(field)) return "单位"; + if ("quantity".equals(field)) return "数量"; + if ("expectedDate".equals(field)) return "期望到货日期"; + if ("purchaseLink".equals(field)) return "采购链接"; + if ("productRemark".equals(field)) return "产品备注"; + if ("remark".equals(field)) return "申请备注"; + return field; + } + + private ProductInfo matchProduct(JSONObject item, List products) + { + String code = item.getString("productCode"); + if (StringUtils.isNotBlank(code)) + { + for (ProductInfo product : products) + { + if (same(code, product.getProductCode())) + { + return product; + } + } + } + List matches = new ArrayList<>(); + for (ProductInfo product : products) + { + if (same(item.getString("materialName"), product.getProductName()) + && (StringUtils.isBlank(item.getString("materialParams")) || same(item.getString("materialParams"), product.getSpec()))) + { + matches.add(product); + } + } + return matches.size() == 1 ? matches.get(0) : null; + } + + private void appendTip(JSONObject item, String tip) + { + String current = item.getString("smartTip"); + item.put("smartTip", StringUtils.isBlank(current) ? tip : current + ";" + tip); + } + + private boolean same(String left, String right) + { + return StringUtils.isNotBlank(left) && StringUtils.isNotBlank(right) && left.trim().equalsIgnoreCase(right.trim()); + } + + private JSONObject message(String role, Object content) + { + return JSONObject.of("role", role, "content", content); + } + + private String systemPrompt() + { + return "你是采购清单智能整理器,只返回JSON对象。输出格式为{items:[...]},每项字段必须包含" + + "productId,productCode,materialName,materialBrand,category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark,inferredFields,warning。" + + "识别任意中文或英文表头并正确映射,不能因列顺序不同而错位。单价、总价和申请人不是导入字段,不要混入其他字段。" + + "参数规格中若含品牌或明确型号,可补全materialBrand;能从知名型号可靠判断品牌时允许推断,否则品牌留空。" + + "category可合理归类,quantity缺失填null,expectedDate统一yyyy-MM-dd。" + + "优先从existingProducts中选择完全一致的物料并返回其productId和productCode,不要把相似但型号不同的物料关联。" + + "inferredFields列出所有不是源表直接提供、而是系统匹配或AI推断得到的字段名;不确定信息写入warning。最多返回100项。"; + } + + private String normalizeChatEndpoint(String configuredUrl) + { + String url = StringUtils.defaultString(configuredUrl).trim(); + while (url.endsWith("/")) url = url.substring(0, url.length() - 1); + if (url.endsWith("/v1")) return url + "/chat/completions"; + if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions")) return url; + return url + "/v1/chat/completions"; + } + + private String extractJson(String output) + { + int start = output.indexOf('{'); + int end = output.lastIndexOf('}'); + return start >= 0 && end > start ? output.substring(start, end + 1) : output; + } + + private boolean isExcel(String extension) + { + return "xls".equals(extension) || "xlsx".equals(extension); + } + + private void validateFile(MultipartFile file) + { + if (file == null || file.isEmpty()) throw new ServiceException("请选择图片或Excel文件"); + if (file.getSize() > MAX_FILE_SIZE) throw new ServiceException("文件不能超过10MB"); + String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT); + if (!List.of("xls", "xlsx", "png", "jpg", "jpeg", "webp").contains(extension)) + { + throw new ServiceException("仅支持 xls、xlsx、png、jpg、jpeg、webp 文件"); + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/WmsBatchImportServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/WmsBatchImportServiceImpl.java index 3e30118..e992938 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/WmsBatchImportServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/warehouse/service/impl/WmsBatchImportServiceImpl.java @@ -79,13 +79,13 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService private static final int ITEM_DONE = 3; private static final int ITEM_FAILED = 4; - @Value("${agent.openai.base-url}") + @Value("${agent.model.base-url}") private String baseUrl; - @Value("${agent.openai.api-key}") + @Value("${agent.model.api-key:}") private String apiKey; - @Value("${agent.openai.model}") + @Value("${agent.model.model}") private String model; @Autowired @@ -381,10 +381,6 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService private List parseImage(WmsBatchJob job, String username) throws Exception { - if (StringUtils.isBlank(apiKey)) - { - throw new ServiceException("图片导入需要配置 OPENAI_API_KEY 环境变量"); - } updateJob(job.getId(), JOB_PARSING, 30, null); String output = callVisionModel(job); updateJob(job.getId(), JOB_PARSING, 70, null); @@ -560,36 +556,34 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService JSONObject body = new JSONObject(); body.put("model", model); - JSONArray input = new JSONArray(); - input.add(message("system", visionSystemPrompt())); + JSONArray messages = new JSONArray(); + messages.add(message("system", visionSystemPrompt())); JSONArray content = new JSONArray(); JSONObject text = new JSONObject(); - text.put("type", "input_text"); + text.put("type", "text"); text.put("text", visionUserPrompt()); content.add(text); JSONObject imageContent = new JSONObject(); - imageContent.put("type", "input_image"); - imageContent.put("image_url", "data:" + mimeType + ";base64," + encoded); + imageContent.put("type", "image_url"); + imageContent.put("image_url", JSONObject.of("url", "data:" + mimeType + ";base64," + encoded)); content.add(imageContent); - input.add(message("user", content)); - body.put("input", input); + messages.add(message("user", content)); + body.put("messages", messages); + body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false)); + body.put("temperature", 0); + body.put("max_tokens", 8192); + body.put("stream", false); - 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() + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)) + .version(HttpClient.Version.HTTP_1_1).build(); + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(normalizeEndpoint(baseUrl))) - .timeout(Duration.ofSeconds(60)) - .header("Authorization", "Bearer " + apiKey) + .timeout(Duration.ofSeconds(120)) .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)); + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8)); + if (StringUtils.isNotBlank(apiKey)) requestBuilder.header("Authorization", "Bearer " + apiKey); + HttpResponse response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new ServiceException("图片模型解析失败,HTTP " + response.statusCode()); @@ -1207,25 +1201,34 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService private String normalizeEndpoint(String configuredBaseUrl) { - String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim(); + String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:14081").trim(); while (url.endsWith("/")) { url = url.substring(0, url.length() - 1); } if (url.endsWith("/v1")) { - return url + "/responses"; + return url + "/chat/completions"; } - if (url.endsWith("/v1/responses")) + if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions")) { return url; } - return url + "/v1/responses"; + return url + "/v1/chat/completions"; } private String extractOutputText(String responseBody) { JSONObject root = JSON.parseObject(responseBody); + JSONArray choices = root.getJSONArray("choices"); + if (choices != null && !choices.isEmpty()) + { + JSONObject message = choices.getJSONObject(0).getJSONObject("message"); + if (message != null && StringUtils.isNotBlank(message.getString("content"))) + { + return message.getString("content"); + } + } String outputText = root.getString("output_text"); if (StringUtils.isNotBlank(outputText)) { diff --git a/ruoyi-system/src/main/resources/mapper/warehouse/InRecordMapper.xml b/ruoyi-system/src/main/resources/mapper/warehouse/InRecordMapper.xml index 52cb41e..cc46b5e 100644 --- a/ruoyi-system/src/main/resources/mapper/warehouse/InRecordMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/warehouse/InRecordMapper.xml @@ -6,6 +6,9 @@ + + + @@ -26,7 +29,8 @@ - select ir.id, ir.in_no, ir.area_id, ir.product_id, ir.in_type, ir.in_num, ir.in_status, + select ir.id, ir.in_no, ir.area_id, ir.product_id, ir.purchase_request_id, ir.arrival_id, ir.source_no, + ir.in_type, ir.in_num, ir.in_status, ir.operator, ir.in_operator, ir.confirm_time, ir.remark, ir.create_time, ir.update_time, wa.area_code, wa.area_name, pi.product_code, pi.product_name, pi.brand, pi.category, pi.spec, pi.unit from in_record ir @@ -57,17 +61,26 @@ + + - insert into in_record(in_no, area_id, product_id, in_type, in_num, in_status, operator, in_operator, remark, create_time) - values(#{inNo}, #{areaId}, #{productId}, #{inType}, #{inNum}, #{inStatus}, #{operator}, #{inOperator}, #{remark}, sysdate()) + insert into in_record(in_no, area_id, product_id, purchase_request_id, arrival_id, source_no, in_type, in_num, + in_status, operator, in_operator, remark, create_time) + values(#{inNo}, #{areaId}, #{productId}, #{purchaseRequestId}, #{arrivalId}, #{sourceNo}, #{inType}, #{inNum}, + #{inStatus}, #{operator}, #{inOperator}, #{remark}, sysdate()) @@ -96,4 +109,8 @@ delete from in_record where in_status = 0 and id in #{id} + + + delete from in_record where arrival_id = #{arrivalId} and in_status = 0 + diff --git a/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseArrivalMapper.xml b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseArrivalMapper.xml new file mode 100644 index 0000000..9eaf256 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseArrivalMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select pa.id, pa.arrival_no, pa.purchase_request_id, pa.arrival_quantity, pa.arrival_date, + pa.acceptance_status, pa.acceptance_remark, pa.accepted_by, pa.accepted_time, + pa.pre_accept_purchase_status, pa.revoked_by, pa.revoked_time, pa.revoke_remark, + pa.remark, pa.create_by, pa.create_time, pa.update_by, pa.update_time, + pr.request_no, pr.applicant_id, pr.applicant_name, pr.product_id, + pi.product_code, pr.material_name, pr.material_brand, pr.material_params, pr.unit + from purchase_arrival pa + join purchase_request pr on pr.id = pa.purchase_request_id + left join product_info pi on pi.id = pr.product_id + + + + + + + + + + + + + + + + insert into purchase_arrival(arrival_no, purchase_request_id, arrival_quantity, arrival_date, + acceptance_status, remark, create_by, create_time) + values(#{arrivalNo}, #{purchaseRequestId}, #{arrivalQuantity}, #{arrivalDate}, 0, + #{remark}, #{createBy}, sysdate()) + + + + update purchase_arrival + set acceptance_status = #{acceptanceStatus}, acceptance_remark = #{acceptanceRemark}, + accepted_by = #{acceptedBy}, accepted_time = #{acceptedTime}, + pre_accept_purchase_status = #{preAcceptPurchaseStatus}, + update_by = #{updateBy}, update_time = sysdate() + where id = #{id} and acceptance_status = 0 + + + + update purchase_arrival + set acceptance_status = 0, + acceptance_remark = null, accepted_by = null, accepted_time = null, + revoked_by = #{revokedBy}, revoked_time = #{revokedTime}, revoke_remark = #{revokeRemark}, + update_by = #{updateBy}, update_time = sysdate() + where id = #{id} and acceptance_status in (1, 2) + + + + delete from purchase_arrival where id = #{id} and acceptance_status = 0 + + diff --git a/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseBatchMapper.xml b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseBatchMapper.xml new file mode 100644 index 0000000..f109a0b --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseBatchMapper.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into purchase_batch(batch_no, batch_name, purchase_status, progress_remark, create_by, create_time) + values(#{batchNo}, #{batchName}, #{purchaseStatus}, #{progressRemark}, #{createBy}, sysdate()) + + + + update purchase_batch + set batch_no = #{batchNo}, batch_name = #{batchName}, purchase_status = #{purchaseStatus}, + progress_remark = #{progressRemark}, update_by = #{updateBy}, update_time = sysdate() + where id = #{id} + + + + + + + + update purchase_request + set purchase_batch_id = #{batch.id}, purchase_batch_no = #{batch.batchNo}, + update_by = #{updateBy}, update_time = sysdate() + where id in + #{id} + + + + update purchase_request + set purchase_batch_no = #{batchNo}, update_by = #{updateBy}, update_time = sysdate() + where purchase_batch_id = #{batchId} + + + + delete from purchase_batch where id = #{id} + + diff --git a/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseRequestMapper.xml b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseRequestMapper.xml new file mode 100644 index 0000000..b72d40f --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/warehouse/PurchaseRequestMapper.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select pr.id, pr.request_no, pr.product_id, pr.purchase_batch_id, pr.material_name, pr.material_brand, pr.category, pr.material_params, + pr.purchase_link, pr.quantity, pr.unit, pr.expected_date, pr.applicant_id, pr.applicant_name, + pr.purchase_status, pr.purchase_batch_no, pr.progress_remark, pr.remark, + pr.create_by, pr.create_time, pr.update_by, pr.update_time, + pi.product_code, pi.product_name, pi.brand as product_brand, + pi.category as product_category, pi.spec as product_spec, pi.remark as product_remark, + coalesce(pa.accepted_quantity, 0) as accepted_quantity, + coalesce(pa.pending_quantity, 0) as pending_arrival_quantity, + greatest(pr.quantity - coalesce(pa.accepted_quantity, 0) - coalesce(pa.pending_quantity, 0), 0) as remaining_quantity + from purchase_request pr + left join product_info pi on pi.id = pr.product_id + left join ( + select purchase_request_id, + sum(case when acceptance_status = 1 then arrival_quantity else 0 end) as accepted_quantity, + sum(case when acceptance_status = 0 then arrival_quantity else 0 end) as pending_quantity + from purchase_arrival + group by purchase_request_id + ) pa on pa.purchase_request_id = pr.id + + + + + + + + + + + + insert into purchase_request( + request_no, product_id, material_name, material_brand, category, material_params, purchase_link, + quantity, unit, expected_date, applicant_id, applicant_name, purchase_status, purchase_batch_no, + progress_remark, remark, create_by, create_time + ) values ( + #{requestNo}, #{productId}, #{materialName}, #{materialBrand}, #{category}, #{materialParams}, #{purchaseLink}, + #{quantity}, #{unit}, #{expectedDate}, #{applicantId}, #{applicantName}, #{purchaseStatus}, #{purchaseBatchNo}, + #{progressRemark}, #{remark}, #{createBy}, sysdate() + ) + + + + update purchase_request + + request_no = #{purchaseRequest.requestNo}, + product_id = #{purchaseRequest.productId}, + material_name = #{purchaseRequest.materialName}, + material_brand = #{purchaseRequest.materialBrand}, + category = #{purchaseRequest.category}, + material_params = #{purchaseRequest.materialParams}, + purchase_link = #{purchaseRequest.purchaseLink}, + quantity = #{purchaseRequest.quantity}, + unit = #{purchaseRequest.unit}, + expected_date = #{purchaseRequest.expectedDate}, + applicant_id = #{purchaseRequest.applicantId}, + applicant_name = #{purchaseRequest.applicantName}, + purchase_status = #{purchaseRequest.purchaseStatus}, + purchase_batch_no = #{purchaseRequest.purchaseBatchNo}, + progress_remark = #{purchaseRequest.progressRemark}, + remark = #{purchaseRequest.remark}, + update_by = #{purchaseRequest.updateBy}, + update_time = sysdate() + + where id = #{purchaseRequest.id} + and applicant_id = #{applicantId} + + + + delete from purchase_request where id in + #{id} + and applicant_id = #{applicantId} + + + + update purchase_request + set purchase_status = #{purchaseStatus}, + progress_remark = #{progressRemark}, + update_by = #{updateBy}, update_time = sysdate() + where id = #{id} + + diff --git a/ruoyi-ui/src/api/warehouse/purchase.js b/ruoyi-ui/src/api/warehouse/purchase.js new file mode 100644 index 0000000..f3f1ad9 --- /dev/null +++ b/ruoyi-ui/src/api/warehouse/purchase.js @@ -0,0 +1,87 @@ +import request from '@/utils/request' + +export function listPurchase(query) { + return request({ url: '/warehouse/purchase/list', method: 'get', params: query }) +} + +export function getPurchase(id) { + return request({ url: '/warehouse/purchase/' + id, method: 'get' }) +} + +export function addPurchase(data) { + return request({ url: '/warehouse/purchase', method: 'post', data }) +} + +export function tableAddPurchase(data) { + return request({ url: '/warehouse/purchase/table', method: 'post', data }) +} + +export function smartImportPurchase(data) { + return request({ + url: '/warehouse/purchase/smart-import', + method: 'post', + data, + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, + timeout: 300000 + }) +} + +export function updatePurchase(data) { + return request({ url: '/warehouse/purchase', method: 'put', data }) +} + +export function updatePurchaseStatus(id, data) { + return request({ url: '/warehouse/purchase/' + id + '/status', method: 'put', data }) +} + +export function listPurchaseArrival(requestId) { + return request({ url: '/warehouse/purchase/arrival/' + requestId, method: 'get' }) +} + +export function addPurchaseArrival(data) { + return request({ url: '/warehouse/purchase/arrival', method: 'post', data }) +} + +export function acceptPurchaseArrival(id, data) { + return request({ url: '/warehouse/purchase/arrival/' + id + '/accept', method: 'put', data }) +} + +export function revokePurchaseArrival(id, data) { + return request({ url: '/warehouse/purchase/arrival/' + id + '/revoke', method: 'put', data }) +} + +export function delPurchaseArrival(id) { + return request({ url: '/warehouse/purchase/arrival/' + id, method: 'delete' }) +} + +export function listPurchaseBatch(query) { + return request({ url: '/warehouse/purchase/batch/list', method: 'get', params: query }) +} + +export function optionPurchaseBatch(keyword) { + return request({ url: '/warehouse/purchase/batch/options', method: 'get', params: { keyword } }) +} + +export function filterOptionPurchaseBatch(keyword) { + return request({ url: '/warehouse/purchase/batch/filter-options', method: 'get', params: { keyword } }) +} + +export function addPurchaseBatch(data) { + return request({ url: '/warehouse/purchase/batch', method: 'post', data }) +} + +export function updatePurchaseBatch(data) { + return request({ url: '/warehouse/purchase/batch', method: 'put', data }) +} + +export function assignPurchaseBatch(data) { + return request({ url: '/warehouse/purchase/batch/assign', method: 'put', data }) +} + +export function delPurchaseBatch(id) { + return request({ url: '/warehouse/purchase/batch/' + id, method: 'delete' }) +} + +export function delPurchase(ids) { + return request({ url: '/warehouse/purchase/' + ids, method: 'delete' }) +} diff --git a/ruoyi-ui/src/components/ExcelImportDialog/index.vue b/ruoyi-ui/src/components/ExcelImportDialog/index.vue index 1d32764..f70b2b7 100644 --- a/ruoyi-ui/src/components/ExcelImportDialog/index.vue +++ b/ruoyi-ui/src/components/ExcelImportDialog/index.vue @@ -1,10 +1,10 @@