diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tyre/BaseInventoryController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tyre/BaseInventoryController.java index f287f8c9..4e74141a 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tyre/BaseInventoryController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tyre/BaseInventoryController.java @@ -6,9 +6,11 @@ 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.ShiroUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.domain.BaseInventory; import com.ruoyi.system.domain.BaseTyre; +import com.ruoyi.system.domain.vo.InboundBatchPreviewVo; import com.ruoyi.system.service.IBaseInventoryService; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; @@ -137,4 +139,64 @@ public class BaseInventoryController extends BaseController { return baseInventoryService.OutInventoryByPda(baseInventory,baseTyre); } + + /** + * 批量入库预览接口:按入库编码查询批次下的轮胎列表,并统计已入库 / 待入库数量。 + * + * @param inboundCode 入库编码 + * @return 批次预览结果 + */ + @RequiresPermissions("tyre:inventory:add") + @PostMapping("/batchInbound/preview") + @ResponseBody + public AjaxResult batchInboundPreview(@RequestParam("inboundCode") String inboundCode) + { + InboundBatchPreviewVo data = baseInventoryService.previewBatchByCode(inboundCode); + return AjaxResult.success("批次预览成功", data); + } + + /** + * 批量入库执行接口:按入库编码将批次内所有待入库轮胎写入库存,逐条独立事务保证失败隔离。 + * + * @param inboundCode 入库编码 + * @return 入库处理结果 + */ + @RequiresPermissions("tyre:inventory:add") + @Log(title = "轮胎批量入库", businessType = BusinessType.IMPORT) + @PostMapping("/batchInbound") + @ResponseBody + public AjaxResult batchInboundSave(@RequestParam("inboundCode") String inboundCode) + { + int rows = baseInventoryService.batchInboundByCode(inboundCode, ShiroUtils.getLoginName()); + return rows > 0 ? AjaxResult.success("批量入库完成,成功处理 " + rows + " 条", rows) : AjaxResult.error("批量入库失败或无可入库轮胎"); + } + + /** + * 批量入库撤回接口:按入库编码撤回批次入库,仅处理当前仍在库状态的记录。 + * + * @param inboundCode 入库编码 + * @return 撤回处理结果 + */ + @RequiresPermissions("tyre:inventory:add") + @Log(title = "轮胎批量入库撤回", businessType = BusinessType.UPDATE) + @PostMapping("/batchInbound/rollback") + @ResponseBody + public AjaxResult batchInboundRollback(@RequestParam("inboundCode") String inboundCode) + { + int rows = baseInventoryService.rollbackBatchInboundByCode(inboundCode, ShiroUtils.getLoginName()); + return rows > 0 ? AjaxResult.success("批量撤回完成,成功处理 " + rows + " 条", rows) : AjaxResult.error("批量撤回失败或无可撤回轮胎"); + } + + /** + * 批量入库页面路由:返回批量入库 Thymeleaf 模板视图名。 + * + * @return 模板路径 + */ + @RequiresPermissions("tyre:inventory:add") + @GetMapping("/batchInbound") + public String batchInbound() + { + return prefix + "/batchInbound"; + } + } diff --git a/ruoyi-admin/src/main/resources/templates/tyre/inventory/batchInbound.html b/ruoyi-admin/src/main/resources/templates/tyre/inventory/batchInbound.html new file mode 100644 index 00000000..0f6b6852 --- /dev/null +++ b/ruoyi-admin/src/main/resources/templates/tyre/inventory/batchInbound.html @@ -0,0 +1,181 @@ + + + + + + + +
+
+
+
+
+ + +
+ +
+
+ + +
+
+ + + + diff --git a/ruoyi-admin/src/main/resources/templates/tyre/inventory/inventory.html b/ruoyi-admin/src/main/resources/templates/tyre/inventory/inventory.html index db030c94..19931038 100644 --- a/ruoyi-admin/src/main/resources/templates/tyre/inventory/inventory.html +++ b/ruoyi-admin/src/main/resources/templates/tyre/inventory/inventory.html @@ -62,6 +62,9 @@ 导出 + + 批量入库 +
@@ -154,6 +157,11 @@ $.table.init(options); }); + function openBatchInbound() { + // 批量入库归并在库存信息入口下,仍用独立页签承载预览/入库/撤回,避免挤占库存列表操作区。 + $.modal.openTab("轮胎批量入库", prefix + "/batchInbound"); + } + /* 用户管理-新增-选择部门树 */ function selectDeptTree() { var treeId = $("#treeId").val(); @@ -186,4 +194,4 @@ } - \ No newline at end of file + diff --git a/ruoyi-admin/src/main/resources/templates/tyre/tyre/tyre.html b/ruoyi-admin/src/main/resources/templates/tyre/tyre/tyre.html index 993dd142..cb0c7b47 100644 --- a/ruoyi-admin/src/main/resources/templates/tyre/tyre/tyre.html +++ b/ruoyi-admin/src/main/resources/templates/tyre/tyre/tyre.html @@ -92,6 +92,7 @@ var removeFlag = [[${@permission.hasPermi('tyre:tyre:remove')}]]; var prefix = ctx + "tyre/tyre"; var datas = [[${@dict.getType('tyre_type')}]]; + var inventoryStatusDict = [[${@dict.getType('inventory_status')}]]; // 1. 定义点击后的处理函数 function showDetail(tyreId) { // 这里使用了若依框架常用的 $.modal.open 方法 @@ -161,6 +162,17 @@ formatter: function(value, row, index) { return $.table.selectDictLabel(datas, value); } + }, + { + field : 'inventoryStatus', + title : '库存状态', + formatter: function(value, row, index) { + // 约定:base_tyre.inventory_status 为空表示“已发货待入库”(批量导入默认态),非空按字典 inventory_status 翻译 + if ($.common.isEmpty(value)) { + return '已发货待入库'; + } + return $.table.selectDictLabel(inventoryStatusDict, value); + } }, { field : 'carNo', diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/BaseTyre.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/BaseTyre.java index ad9eb305..5ed8c8ca 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/BaseTyre.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/BaseTyre.java @@ -35,6 +35,10 @@ public class BaseTyre extends BaseEntity @Excel(name = "RFID标签") private String tyreEpc; + /** 入库编码 */ + @Excel(name = "入库编码", prompt = "第一行必填;后续空行继承最近一次出现的编码;中途填写新编码则从该行开始切换批次。") + private String inboundCode; + /** 轮胎品牌 */ @Excel(name = "轮胎品牌") private String tyreBrand; @@ -74,6 +78,11 @@ public class BaseTyre extends BaseEntity @Excel(name = "所在轮位") private String wheelPostion; + /** + * 库存状态(字典 inventory_status)。 + * 约定:批量导入保持为空;空值在页面统一展示为“已发货待入库”,非空按字典翻译。 + */ + private String inventoryStatus; public String getCompany() { return company; @@ -117,6 +126,15 @@ public class BaseTyre extends BaseEntity this.selfNo = selfNo; } + public String getInboundCode() { + return inboundCode; + } + + public void setInboundCode(String inboundCode) { + // 供应商 Excel 容易带前后空格,统一收口避免同一批次因空格拆成两个编码。 + this.inboundCode = inboundCode == null ? null : inboundCode.trim(); + } + public Long getDeptId() { return deptId; } @@ -141,6 +159,14 @@ public class BaseTyre extends BaseEntity this.wheelPostion = wheelPostion; } + public String getInventoryStatus() { + return inventoryStatus; + } + + public void setInventoryStatus(String inventoryStatus) { + this.inventoryStatus = inventoryStatus; + } + public void setTyreId(Long tyreId) { this.tyreId = tyreId; @@ -242,6 +268,8 @@ public class BaseTyre extends BaseEntity .append("tyreNo", getTyreNo()) .append("tyreEpc", getTyreEpc()) + + .append("inboundCode", getInboundCode()) .append("tyreBrand", getTyreBrand()) @@ -256,6 +284,8 @@ public class BaseTyre extends BaseEntity .append("team", getTeam()) .append("deptName", getDeptName()) + + .append("inventoryStatus", getInventoryStatus()) .append("createBy", getCreateBy()) diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BaseTyreVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BaseTyreVo.java index 43b2d4d7..24e61c12 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BaseTyreVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BaseTyreVo.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import org.apache.poi.ss.usermodel.IndexedColors; public class BaseTyreVo { private static final long serialVersionUID = 1L; @@ -38,6 +39,13 @@ public class BaseTyreVo { @Excel(name = "轮胎沟槽数") private String grooves; + /** 入库编码 */ + @Excel(name = "入库编码", color = IndexedColors.RED, + prompt = "Excel 第一行必须填写默认入库编码,后续空行自动沿用最近一次出现的编码;" + + "如中途填写新编码,则从该行开始切换为新批次,直到再次填写其他编码。" + + "Excel 第一行必须填写默认入库编码,后续空行自动沿用最近一次出现的编码;如中途填写新编码,则从该行开始切换为新批次,直到再次填写其他编码。") + private String inboundCode; + public @NotBlank(message = "胎号不能为空") String getTyreNo() { return tyreNo; } @@ -54,6 +62,15 @@ public class BaseTyreVo { this.tyreEpc = tyreEpc; } + public String getInboundCode() { + return inboundCode; + } + + public void setInboundCode(String inboundCode) { + // 入库编码是批量入库的业务键,导入阶段先去空格,避免后续按编码查询不到。 + this.inboundCode = inboundCode == null ? null : inboundCode.trim(); + } + public String getTyreBrand() { return tyreBrand; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewItemVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewItemVo.java new file mode 100644 index 00000000..52cbfb93 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewItemVo.java @@ -0,0 +1,69 @@ +package com.ruoyi.system.domain.vo; + +/** + * 批量入库预览明细。 + * + * @author zch + */ +public class InboundBatchPreviewItemVo +{ + private String tyreEpc; // 轮胎 RFID/EPC 唯一标识 + + private String tyreNo; // 轮胎外部编号 + + private String tyreBrand; // 轮胎品牌 + + private String tyreModel; // 轮胎型号 + + private String status; // 状态:待入库 / 已存在库存记录 + + public String getTyreEpc() + { + return tyreEpc; + } + + public void setTyreEpc(String tyreEpc) + { + this.tyreEpc = tyreEpc; + } + + public String getTyreNo() + { + return tyreNo; + } + + public void setTyreNo(String tyreNo) + { + this.tyreNo = tyreNo; + } + + public String getTyreBrand() + { + return tyreBrand; + } + + public void setTyreBrand(String tyreBrand) + { + this.tyreBrand = tyreBrand; + } + + public String getTyreModel() + { + return tyreModel; + } + + public void setTyreModel(String tyreModel) + { + this.tyreModel = tyreModel; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewVo.java new file mode 100644 index 00000000..4401348e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InboundBatchPreviewVo.java @@ -0,0 +1,71 @@ +package com.ruoyi.system.domain.vo; + +import java.util.List; + +/** + * 批量入库预览结果。 + * + * @author zch + */ +public class InboundBatchPreviewVo +{ + private String inboundCode; // 入库编码 + + private int total; // 批次轮胎总数 + + private int exists; // 已存在库存记录的数量 + + private int pending; // 待入库数量(total - exists) + + private List items; // 批次明细列表 + + public String getInboundCode() + { + return inboundCode; + } + + public void setInboundCode(String inboundCode) + { + this.inboundCode = inboundCode; + } + + public int getTotal() + { + return total; + } + + public void setTotal(int total) + { + this.total = total; + } + + public int getExists() + { + return exists; + } + + public void setExists(int exists) + { + this.exists = exists; + } + + public int getPending() + { + return pending; + } + + public void setPending(int pending) + { + this.pending = pending; + } + + public List getItems() + { + return items; + } + + public void setItems(List items) + { + this.items = items; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseInventoryMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseInventoryMapper.java index bb821c18..97ddeb9d 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseInventoryMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseInventoryMapper.java @@ -4,6 +4,7 @@ package com.ruoyi.system.mapper; import com.ruoyi.common.core.domain.entity.SysDeptVo; import com.ruoyi.system.domain.BaseInventory; +import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; @@ -73,4 +74,14 @@ public interface BaseInventoryMapper int queryInCar(); int queryCarTotal(); + + + /** + * 根据轮胎RFID删除在库库存信息 + * + * @param tyreRfid 轮胎RFID + * @return 结果 + */ + int deleteInStockBaseInventoryByEpc(@Param("tyreRfid") String tyreRfid); + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseTyreMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseTyreMapper.java index 9d96973b..02bc1097 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseTyreMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BaseTyreMapper.java @@ -5,6 +5,7 @@ package com.ruoyi.system.mapper; import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.system.domain.BaseTyre; import com.ruoyi.system.domain.vo.BaseTyreVo; +import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; @@ -72,4 +73,21 @@ public interface BaseTyreMapper String getTeamByUser(String createBy); List vTyreStockSummary(SysDept sysDept); + + /** + * 根据入库编码查询批次下的轮胎基础信息列表 + * + * @param inboundCode 入库编码 + * @return 轮胎基础信息列表 + */ + List selectByInboundCode(@Param("inboundCode") String inboundCode); + + /** + * 根据入库编码统计批次下的轮胎数量 + * + * @param inboundCode 入库编码 + * @return 轮胎数量 + */ + int countBaseTyreByInboundCode(@Param("inboundCode") String inboundCode); + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseInventoryService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseInventoryService.java index 09981f26..a1796326 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseInventoryService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseInventoryService.java @@ -4,6 +4,7 @@ import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysDeptVo; import com.ruoyi.system.domain.BaseInventory; import com.ruoyi.system.domain.BaseTyre; +import com.ruoyi.system.domain.vo.InboundBatchPreviewVo; import java.util.List; @@ -74,4 +75,31 @@ public interface IBaseInventoryService int queryInCar(); int queryCarTotal(); + + /** + * 按入库编码预览批次入库情况,返回批次轮胎总数、已入库数、待入库数及明细列表。 + * + * @param inboundCode 入库编码 + * @return 批量入库预览视图 + */ + InboundBatchPreviewVo previewBatchByCode(String inboundCode); + + /** + * 按入库编码执行批量入库,逐条使用独立事务写入库存并同步轮胎归属,失败条目不影响其他条目。 + * + * @param inboundCode 入库编码 + * @param operName 操作人登录名 + * @return 实际成功入库的条数 + */ + int batchInboundByCode(String inboundCode, String operName); + + /** + * 按入库编码撤回批量入库,仅删除在库状态的库存记录并写入撤回流水。 + * + * @param inboundCode 入库编码 + * @param operName 操作人登录名 + * @return 实际成功撤回的条数 + */ + int rollbackBatchInboundByCode(String inboundCode, String operName); + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseTyreService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseTyreService.java index 6d8e0e4d..46281116 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseTyreService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBaseTyreService.java @@ -75,4 +75,13 @@ public interface IBaseTyreService String importBaseTyre(List baseTyreVoList, boolean updateSupport, String operName); List vTyreStockSummary(SysDept sysDept); + + /** + * 根据入库编码查询该批次下的所有轮胎基础档案。 + * + * @param inboundCode 入库编码 + * @return 该批次下的轮胎基础档案列表 + */ + List selectBaseTyresByInboundCode(String inboundCode); + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IInboundBatchService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IInboundBatchService.java new file mode 100644 index 00000000..66af5e64 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IInboundBatchService.java @@ -0,0 +1,31 @@ +package com.ruoyi.system.service; + +import com.ruoyi.system.domain.BaseTyre; + +/** + * 批量入库单条事务服务接口 + * + * @author zch + */ +public interface IInboundBatchService +{ + /** + * 单条轮胎入库。 + * + * @param tyre 轮胎基础档案 + * @param operName 操作人登录名 + * @param inboundCode 入库编码 + * @return true=已入库,false=已存在需跳过 + */ + boolean inboundOne(BaseTyre tyre, String operName, String inboundCode); + + /** + * 单条轮胎撤回入库。 + * + * @param tyre 轮胎基础档案 + * @param operName 操作人登录名 + * @param inboundCode 入库编码 + * @return true=已撤回,false=无可撤回库存需跳过 + */ + boolean rollbackOne(BaseTyre tyre, String operName, String inboundCode); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseInventoryServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseInventoryServiceImpl.java index c9ae9188..896b6aee 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseInventoryServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseInventoryServiceImpl.java @@ -5,21 +5,27 @@ import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysDeptVo; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.BaseInventory; import com.ruoyi.system.domain.BaseTyre; import com.ruoyi.system.domain.RecordWarehousing; +import com.ruoyi.system.domain.vo.InboundBatchPreviewItemVo; +import com.ruoyi.system.domain.vo.InboundBatchPreviewVo; import com.ruoyi.system.mapper.BaseInventoryMapper; import com.ruoyi.system.mapper.BaseTyreMapper; import com.ruoyi.system.mapper.RecordWarehousingMapper; import com.ruoyi.system.mapper.SysUserMapper; import com.ruoyi.system.service.IBaseInventoryService; +import com.ruoyi.system.service.IInboundBatchService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -42,6 +48,10 @@ public class BaseInventoryServiceImpl implements IBaseInventoryService @Autowired private SysUserMapper sysUserMapper; + + @Autowired + private IInboundBatchService inboundBatchService; + private static final Logger log = LoggerFactory.getLogger(BaseInventoryServiceImpl.class); /** @@ -222,4 +232,167 @@ public class BaseInventoryServiceImpl implements IBaseInventoryService public int queryCarTotal() { return baseInventoryMapper.queryCarTotal(); } + + + /** + * 按入库编码预览批次入库情况 + * + * @param inboundCode 入库编码 + * @return 批量入库预览视图 + */ + @Override + public InboundBatchPreviewVo previewBatchByCode(String inboundCode) { + if (StringUtils.isEmpty(inboundCode)) + { + throw new ServiceException("入库编码不能为空"); // 前置参数校验 + } + String code = normalizeInboundCode(inboundCode); // 标准化入库编码,去除前后空白 + List tyres = baseTyreMapper.selectByInboundCode(code); // 按编码查询批次下的所有轮胎基础档案 + if (StringUtils.isEmpty(tyres)) + { + throw new ServiceException("未找到入库编码为 " + code + " 的批次"); // 无数据时给出明确提示 + } + // Service 层只返回业务数据,避免把 AjaxResult 这种 Web 响应模型下沉到业务层。 + return buildBatchPreview(code, tyres); // 组装预览视图,统计总条数、已入库条数与待入库条数 + } + + /** + * 按入库编码执行批量入库 + * + * @param inboundCode 入库编码 + * @param operName 操作人登录名 + * @return 实际成功入库的条数 + */ + @Override + public int batchInboundByCode(String inboundCode, String operName) { + if (StringUtils.isEmpty(inboundCode)) + { + throw new ServiceException("入库编码不能为空"); // 前置参数校验 + } + String code = normalizeInboundCode(inboundCode); // 标准化入库编码 + List tyres = baseTyreMapper.selectByInboundCode(code); // 查询批次下所有轮胎 + if (StringUtils.isEmpty(tyres)) + { + throw new ServiceException("未找到入库编码为 " + code + " 的批次"); // 无数据时阻断 + } + + int success = 0; // 统计实际成功写入库存的条数 + for (BaseTyre tyre : tyres) + { + try + { + boolean inserted = inboundBatchService.inboundOne(tyre, operName, code); // 逐条调用独立事务入库 + if (inserted) + { + success++; // 写入成功则计数累加 + } + else + { + // 已存在库存时按幂等跳过,不计入本次成功写入条数。 + log.info("批量入库跳过已在库轮胎,inboundCode={}, tyreEpc={}", code, tyre.getTyreEpc()); // 记录幂等跳过日志 + } + } + catch (Exception e) + { + log.error("批量入库失败,inboundCode={}, tyreEpc={}", code, tyre.getTyreEpc(), e); // 单条异常不影响整体批次,仅记录日志 + } + } + + return success; // 返回实际成功入库条数 + } + + /** + * 按入库编码撤回批量入库 + * + * @param inboundCode 入库编码 + * @param operName 操作人登录名 + * @return 实际成功撤回的条数 + */ + @Override + public int rollbackBatchInboundByCode(String inboundCode, String operName) { + if (StringUtils.isEmpty(inboundCode)) + { + throw new ServiceException("入库编码不能为空"); // 前置参数校验 + } + String code = normalizeInboundCode(inboundCode); // 标准化入库编码 + List tyres = baseTyreMapper.selectByInboundCode(code); // 查询批次下所有轮胎 + if (StringUtils.isEmpty(tyres)) + { + throw new ServiceException("未找到入库编码为 " + code + " 的批次"); // 无数据时阻断 + } + + int success = 0; // 统计实际成功撤回的条数 + for (BaseTyre tyre : tyres) + { + try + { + boolean rollback = inboundBatchService.rollbackOne(tyre, operName, code); // 逐条调用独立事务撤回 + if (rollback) + { + success++; // 撤回成功则计数累加 + } + else + { + // 不在库或已出库的记录不能撤回,按幂等跳过,不计入成功撤回条数。 + log.info("批量入库撤回跳过非在库轮胎,inboundCode={}, tyreEpc={}", code, tyre.getTyreEpc()); // 记录幂等跳过日志 + } + } + catch (Exception e) + { + log.error("批量入库撤回失败,inboundCode={}, tyreEpc={}", code, tyre.getTyreEpc(), e); // 单条异常不影响整体批次,仅记录日志 + } + } + + return success; // 返回实际成功撤回条数 + } + + /** + * 标准化入库编码,去除前后空白 + * + * @param inboundCode 原始入库编码 + * @return 标准化后的入库编码 + */ + private String normalizeInboundCode(String inboundCode) + { + return inboundCode.trim(); // 去除前后空白,避免首尾空格导致批次查询不一致 + } + + /** + * 组装批量入库预览视图 + * + * @param inboundCode 入库编码 + * @param tyres 批次下的轮胎列表 + * @return 预览视图对象 + */ + private InboundBatchPreviewVo buildBatchPreview(String inboundCode, List tyres) + { + int exists = 0; // 统计已入库的轮胎条数 + List items = new ArrayList<>(); // 明细列表 + for (BaseTyre tyre : tyres) + { + BaseInventory probe = new BaseInventory(); // 构造库存查询探针 + probe.setTyreRfid(tyre.getTyreEpc()); // 按 RFID 查询库存 + BaseInventory inventory = baseInventoryMapper.selectBaseInventoryByEpc(probe); // 查询当前是否已入库 + if (inventory != null) + { + exists++; // 已存在库存记录则累加 + } + InboundBatchPreviewItemVo item = new InboundBatchPreviewItemVo(); // 构造预览明细项 + item.setTyreEpc(tyre.getTyreEpc()); // 轮胎 RFID + item.setTyreNo(tyre.getTyreNo()); // 轮胎外部编号 + item.setTyreBrand(tyre.getTyreBrand()); // 轮胎品牌 + item.setTyreModel(tyre.getTyreModel()); // 轮胎型号 + // 预览时直接给出状态,现场可在入库前确认哪些会被跳过。 + item.setStatus(inventory == null ? "待入库" : "已存在库存记录"); // 状态区分:未入库 / 已存在 + items.add(item); // 加入明细列表 + } + InboundBatchPreviewVo data = new InboundBatchPreviewVo(); // 构造预览结果对象 + data.setInboundCode(inboundCode); // 批次入库编码 + data.setTotal(tyres.size()); // 批次轮胎总数 + data.setExists(exists); // 已入库数量 + data.setPending(tyres.size() - exists); // 待入库数量 = 总数 - 已存在 + data.setItems(items); // 明细数据 + return data; // 返回预览结果 + } + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseTyreServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseTyreServiceImpl.java index d146ac45..37e66bb9 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseTyreServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaseTyreServiceImpl.java @@ -19,8 +19,10 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static com.ruoyi.common.utils.ShiroUtils.getLoginName; @@ -165,6 +167,31 @@ public class BaseTyreServiceImpl implements IBaseTyreService int failureNum = 0; StringBuilder successMsg = new StringBuilder(); StringBuilder failureMsg = new StringBuilder(); + String currentInboundCode = null; // 当前行生效的入库编码,空行时继承上一行 + Set inboundCodes = new HashSet<>(); // 收集本次导入涉及的所有入库编码,用于全局唯一校验 + for (BaseTyreVo baseTyreVo : baseTyreVoList) + { + String rowInboundCode = baseTyreVo.getInboundCode(); // 读取 Excel 当前行的入库编码 + if (StringUtils.isNotEmpty(rowInboundCode)) + { + currentInboundCode = rowInboundCode.trim(); // 遇到非空编码时更新当前生效编码并去空白 + inboundCodes.add(currentInboundCode); // 加入编码集合,后续做全局唯一校验 + } + if (StringUtils.isEmpty(currentInboundCode)) + { + throw new ServiceException("入库编码不能为空,请在导入模板第一行填写默认入库编码!"); // 首行及继承后仍为空则阻断导入 + } + // 入库编码按 Excel 行顺序分段继承:空行沿用最近一次出现的编码,遇到新编码则切换到新批次。 + baseTyreVo.setInboundCode(currentInboundCode); // 回写最终生效的入库编码到当前行 + } + for (String inboundCode : inboundCodes) + { + // 入库编码只按编码本身做全局唯一校验,不绑定供应商,号段由用户自行规划。 + if (baseTyreMapper.countBaseTyreByInboundCode(inboundCode) > 0) + { + throw new ServiceException("入库编码 " + inboundCode + " 已存在,请更换本批次唯一入库编码!"); // 任一编码重复即整体阻断,防止批次混用 + } + } for (BaseTyreVo baseTyreVo : baseTyreVoList) { @@ -213,4 +240,9 @@ public class BaseTyreServiceImpl implements IBaseTyreService List mapList = baseTyreMapper.vTyreStockSummary(sysDept); return mapList; } + + @Override + public List selectBaseTyresByInboundCode(String inboundCode) { + return baseTyreMapper.selectByInboundCode(inboundCode); + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/InboundBatchServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/InboundBatchServiceImpl.java new file mode 100644 index 00000000..52b443ba --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/InboundBatchServiceImpl.java @@ -0,0 +1,159 @@ +package com.ruoyi.system.service.impl; + +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.system.domain.BaseInventory; +import com.ruoyi.system.domain.BaseTyre; +import com.ruoyi.system.domain.RecordWarehousing; +import com.ruoyi.system.mapper.BaseInventoryMapper; +import com.ruoyi.system.mapper.BaseTyreMapper; +import com.ruoyi.system.mapper.RecordWarehousingMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.system.service.IInboundBatchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +/** + * 批量入库单条事务服务 + * + * @author zch + */ +@Service +public class InboundBatchServiceImpl implements IInboundBatchService +{ + @Autowired + private BaseInventoryMapper baseInventoryMapper; // 库存数据访问 + + @Autowired + private BaseTyreMapper baseTyreMapper; // 轮胎基础档案数据访问 + + @Autowired + private RecordWarehousingMapper recordWarehousingMapper; // 入库流水数据访问 + + @Autowired + private SysUserMapper sysUserMapper; // 用户数据访问,用于读取操作人部门 + + /** + * 单条入库使用独立事务,避免批次中某一条失败时把其他轮胎的成功入库一起回滚。 + * + * @param tyre 轮胎基础档案 + * @param operName 操作人登录名 + * @param inboundCode 入库编码 + * @return 入库成功返回 true,已存在库存时幂等跳过返回 false + */ + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public boolean inboundOne(BaseTyre tyre, String operName, String inboundCode) + { + BaseInventory probe = new BaseInventory(); // 构造查询探针 + probe.setTyreRfid(tyre.getTyreEpc()); // 按轮胎 RFID 查找库存 + BaseInventory existed = baseInventoryMapper.selectBaseInventoryByEpc(probe); // 查询是否已入库 + if (existed != null) + { + return false; // 已存在库存记录,幂等跳过 + } + + Date now = DateUtils.getNowDate(); // 获取当前系统时间 + BaseInventory inventory = new BaseInventory(); // 新建库存记录 + inventory.setTyreRfid(tyre.getTyreEpc()); // 绑定轮胎 RFID + inventory.setTyreOutsideId(tyre.getTyreNo()); // 绑定轮胎外部编号 + inventory.setNumber(1L); // 数量默认为 1 + inventory.setStatus("0"); // 0 表示在库状态 + inventory.setCreateBy(operName); // 记录创建人 + inventory.setCreateTime(now); // 记录创建时间 + inventory.setUpdateTime(now); // 记录更新时间 + inventory.setRemark("批量入库[" + inboundCode + "]"); // 备注携带批次号 + int inventoryRows = baseInventoryMapper.insertBaseInventory(inventory); // 写入库存表 + if (inventoryRows <= 0) + { + throw new ServiceException("库存写入失败"); // 写入失败则抛异常触发回滚 + } + + RecordWarehousing record = new RecordWarehousing(); // 新建入库流水 + record.setTyreRfid(tyre.getTyreEpc()); // 绑定轮胎 RFID + record.setType("0"); // 0 表示入库类型 + record.setCreateBy(operName); // 记录操作人 + record.setCreateTime(now); // 记录操作时间 + record.setRemark("批量入库[" + inboundCode + "]"); // 备注携带批次号 + int recordRows = recordWarehousingMapper.insertRecordWarehousing(record); // 写入流水表 + if (recordRows <= 0) + { + throw new ServiceException("入库流水写入失败"); // 写入失败则抛异常触发回滚 + } + syncBaseTyreOwner(tyre, operName, now); // 同步轮胎归属信息到当前操作人部门 + return true; // 单条入库成功 + } + + /** + * 批量入库后同步基础档案归属,保持与 PDA 单条入库一致,避免库存已入库但轮胎仍挂在旧部门。 + */ + private void syncBaseTyreOwner(BaseTyre tyre, String operName, Date now) + { + if (tyre.getTyreId() == null) + { + throw new ServiceException("轮胎基础档案ID不能为空"); // 档案 ID 必传校验 + } + SysUser sysUser = sysUserMapper.selectUserByLoginName(operName); // 按登录名查询用户信息 + if (sysUser == null || sysUser.getDeptId() == null) + { + throw new ServiceException("未找到操作人部门信息"); // 校验操作人及所属部门 + } + + BaseTyre updateTyre = new BaseTyre(); // 构造更新对象 + updateTyre.setTyreId(tyre.getTyreId()); // 指定待更新的轮胎档案 + updateTyre.setTeam(baseTyreMapper.getTeamByUser(operName)); // 同步车队归属 + updateTyre.setDeptId(sysUser.getDeptId()); // 同步部门归属 + updateTyre.setUpdateBy(operName); // 记录更新人 + updateTyre.setUpdateTime(now); // 记录更新时间 + int tyreRows = baseTyreMapper.updateBaseTyre(updateTyre); // 执行更新 + if (tyreRows <= 0) + { + throw new ServiceException("轮胎归属信息同步失败"); // 更新失败抛异常 + } + } + + /** + * 撤回只处理仍处于在库状态的库存记录,避免把已出库或已被后续业务使用的轮胎误删。 + * + * @param tyre 轮胎基础档案 + * @param operName 操作人登录名 + * @param inboundCode 入库编码 + * @return 撤回成功返回 true,库存不存在或非在库状态时幂等跳过返回 false + */ + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public boolean rollbackOne(BaseTyre tyre, String operName, String inboundCode) + { + BaseInventory probe = new BaseInventory(); // 构造查询探针 + probe.setTyreRfid(tyre.getTyreEpc()); // 按轮胎 RFID 查找库存 + BaseInventory existed = baseInventoryMapper.selectBaseInventoryByEpc(probe); // 查询当前库存状态 + if (existed == null || !"0".equals(existed.getStatus())) + { + return false; // 库存不存在或非在库状态时不允许撤回,幂等跳过 + } + + int deleteRows = baseInventoryMapper.deleteInStockBaseInventoryByEpc(tyre.getTyreEpc()); // 删除在库记录 + if (deleteRows <= 0) + { + throw new ServiceException("库存撤回失败"); // 删除失败抛异常 + } + + RecordWarehousing record = new RecordWarehousing(); // 新建撤回流水 + record.setTyreRfid(tyre.getTyreEpc()); // 绑定轮胎 RFID + record.setType("1"); // 1 表示撤回类型 + record.setCreateBy(operName); // 记录操作人 + record.setCreateTime(DateUtils.getNowDate()); // 记录操作时间 + record.setRemark("批量入库撤回[" + inboundCode + "]"); // 备注携带批次号 + int recordRows = recordWarehousingMapper.insertRecordWarehousing(record); // 写入流水表 + if (recordRows <= 0) + { + throw new ServiceException("撤回流水写入失败"); // 写入失败抛异常 + } + return true; // 单条撤回成功 + } +} diff --git a/ruoyi-system/src/main/resources/mapper/tyre/BaseInventoryMapper.xml b/ruoyi-system/src/main/resources/mapper/tyre/BaseInventoryMapper.xml index 7bdbc9d9..106762e3 100644 --- a/ruoyi-system/src/main/resources/mapper/tyre/BaseInventoryMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/tyre/BaseInventoryMapper.xml @@ -143,4 +143,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" delete from base_inventory where tyre_rfid = #{tyreRfid} - \ No newline at end of file + + delete from base_inventory + where tyre_rfid = #{tyreRfid} + and status = '0' + + + diff --git a/ruoyi-system/src/main/resources/mapper/tyre/BaseTyreMapper.xml b/ruoyi-system/src/main/resources/mapper/tyre/BaseTyreMapper.xml index 94e546d0..6ae4f640 100644 --- a/ruoyi-system/src/main/resources/mapper/tyre/BaseTyreMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/tyre/BaseTyreMapper.xml @@ -28,12 +28,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + select d.tyre_id, d.tyre_no, d.self_no, d.tyre_epc, d.tyre_brand, d.tyre_model, d.tyre_level, d.tyre_pattern, - d.grooves, d.pattern_depth, d.tyre_type, d.team, d.inbound_code, d.create_by, d.create_time, d.update_by, d.update_time, + d.grooves, d.pattern_depth, d.tyre_type, d.team, d.inbound_code, d.inventory_status, d.create_by, d.create_time, d.update_by, d.update_time, d.remark, d.car_no, d.wheel_postion, d.dept_id, tyre_dept.dept_name, company_dept.dept_name as company, car_dept.dept_name as carTeam from base_tyre d @@ -67,7 +68,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" d.update_time, d.remark, d.car_no, - wheel_postion + wheel_postion, + d.inventory_status FROM base_tyre d LEFT JOIN sys_user su ON su.login_name = d.create_by @@ -93,6 +95,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND date_format(d.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d') + + AND d.inventory_status = #{inventoryStatus} + ${params.dataScope} @@ -160,6 +165,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" remark, dept_id, pattern_depth, + inventory_status, #{tyreId}, @@ -181,6 +187,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{remark}, #{deptId}, #{patternDepth}, + #{inventoryStatus}, @@ -205,6 +212,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" remark = #{remark}, car_no = #{carNo}, wheel_postion = #{wheelPostion}, + inventory_status = #{inventoryStatus}, where tyre_id = #{tyreId}