feat(轮胎基础信息): 新增轮胎批量入库功能并且新增字段库存状态inventory_status,导入默认为空(显示已发货待入库”)

实现批量入库功能,包括以下核心变更:
1. 新增入库编码字段支持批量导入与分组管理,导入默认为空(显示已发货待入库”)
2. 添加批量入库预览、执行与撤回接口
3. 实现单条独立事务处理保证失败隔离
4. 增加库存状态显示与批量操作权限控制
5. 提供前端批量入库操作页面与结果展示

支持按入库编码对轮胎批次进行统一管理,提升大批量轮胎入库效率
master
zch 1 week ago
parent 26b2e1bbcc
commit 80248fb10b

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

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('轮胎批量入库')" />
<style>
.batch-panel { padding: 15px; }
.batch-actions { margin-top: 10px; }
.batch-summary { margin-top: 15px; }
.batch-table { margin-top: 10px; max-height: 360px; overflow: auto; }
</style>
</head>
<body class="gray-bg">
<div class="container-div batch-panel">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="batchForm" class="form-inline">
<div class="form-group">
<label>入库编码:</label>
<input id="inboundCode" name="inboundCode" type="text" class="form-control" placeholder="请输入入库编码" autocomplete="off"/>
</div>
<div class="batch-actions">
<a class="btn btn-primary" onclick="previewBatch()" shiro:hasPermission="tyre:inventory:batch">
<i class="fa fa-search"></i> 预览批次
</a>
<a class="btn btn-success" onclick="submitBatch()" shiro:hasPermission="tyre:inventory:batch">
<i class="fa fa-upload"></i> 一键入库
</a>
<a class="btn btn-danger" onclick="rollbackBatch()" shiro:hasPermission="tyre:inventory:batchRollback">
<i class="fa fa-undo"></i> 撤回入库
</a>
</div>
</form>
</div>
<div class="col-sm-12 batch-summary" id="summaryPanel" style="display:none;"></div>
<div class="col-sm-12 batch-table" id="detailPanel" style="display:none;"></div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "tyre/inventory";
var processing = false;
function normalizeCode() {
var code = $.trim($("#inboundCode").val());
if (!code) {
$.modal.alertWarning("请输入入库编码");
return "";
}
return code;
}
function previewBatch() {
var code = normalizeCode();
if (!code) {
return;
}
postBatch(prefix + "/batchInbound/preview", code, function (data) {
renderPreview(data);
});
}
function submitBatch() {
var code = normalizeCode();
if (!code) {
return;
}
$.modal.confirm("确认按入库编码 [" + escapeHtml(code) + "] 一键入库?", function () {
// 入库是批量写库存与流水的动作,前端二次确认用于避免现场误点。
postBatch(prefix + "/batchInbound", code, function (data) {
renderResult("批量入库完成", data);
});
});
}
function rollbackBatch() {
var code = normalizeCode();
if (!code) {
return;
}
$.modal.confirm("确认撤回入库编码 [" + escapeHtml(code) + "] 仍在库的库存记录?", function () {
// 撤回只处理仍在库的当前库存记录,已发生后续出库的轮胎由后端跳过。
postBatch(prefix + "/batchInbound/rollback", code, function (data) {
renderResult("批量撤回完成", data);
});
});
}
function postBatch(url, code, callback) {
if (processing) {
return;
}
processing = true;
setButtonsDisabled(true);
$.modal.loading("处理中,请稍候...");
$.ajax({
url: url,
type: "post",
data: { inboundCode: code },
success: function (res) {
if (res.code === 0) {
callback(res.data || {});
} else {
$.modal.alertError(res.msg || "操作失败");
}
},
error: function () {
$.modal.alertError("请求失败,请稍后重试");
},
complete: function () {
$.modal.closeLoading();
processing = false;
setButtonsDisabled(false);
}
});
}
function renderPreview(data) {
var html = "<div class='alert alert-info'>批次:" + escapeHtml(data.inboundCode) +
",共 " + (data.total || 0) + " 条,已存在 " + (data.exists || 0) +
" 条,待入库 " + (data.pending || 0) + " 条</div>";
$("#summaryPanel").html(html).show();
renderItems(data.items || [], "status");
}
function renderResult(title, data) {
if (typeof data === "number") {
// 后端写操作按 RuoYi Service 风格返回影响条数,页面只展示本次实际成功处理数。
$("#summaryPanel").html("<div class='alert alert-success'>" + title + ",成功处理 " + data + " 条</div>").show();
$("#detailPanel").hide().empty();
return;
}
var html = "<div class='alert alert-success'>" + title + ",批次:" + escapeHtml(data.inboundCode) +
",共 " + (data.total || 0) + " 条,成功 " + (data.success || 0) +
" 条,跳过 " + (data.skip || 0) + " 条,失败 " + (data.fail || 0) + " 条</div>";
$("#summaryPanel").html(html).show();
renderFailures(data.failures || []);
}
function renderItems(items, statusField) {
var rows = items.map(function (item) {
return "<tr><td>" + escapeHtml(item.tyreEpc) + "</td><td>" + escapeHtml(item.tyreNo) +
"</td><td>" + escapeHtml(item.tyreBrand) + "</td><td>" + escapeHtml(item.tyreModel) +
"</td><td>" + escapeHtml(item[statusField]) + "</td></tr>";
}).join("");
var table = "<table class='table table-bordered table-striped'><thead><tr><th>RFID标签</th><th>胎号</th><th>品牌</th><th>型号</th><th>状态</th></tr></thead><tbody>" + rows + "</tbody></table>";
$("#detailPanel").html(table).show();
}
function renderFailures(failures) {
if (!failures.length) {
$("#detailPanel").hide().empty();
return;
}
var rows = failures.map(function (item) {
return "<tr><td>" + escapeHtml(item.tyreEpc) + "</td><td>" + escapeHtml(item.tyreNo) +
"</td><td>" + escapeHtml(item.reason) + "</td></tr>";
}).join("");
var table = "<table class='table table-bordered table-striped'><thead><tr><th>RFID标签</th><th>胎号</th><th>失败原因</th></tr></thead><tbody>" + rows + "</tbody></table>";
$("#detailPanel").html(table).show();
}
function setButtonsDisabled(disabled) {
$(".batch-actions .btn").toggleClass("disabled", disabled);
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
$("#batchForm").on("submit", function (e) {
e.preventDefault();
previewBatch();
});
</script>
</body>
</html>

@ -62,6 +62,9 @@
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="tyre:inventory:export">
<i class="fa fa-download"></i> 导出
</a>
<a class="btn btn-info" onclick="openBatchInbound()" shiro:hasPermission="tyre:inventory:batch">
<i class="fa fa-upload"></i> 批量入库
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
@ -154,6 +157,11 @@
$.table.init(options);
});
function openBatchInbound() {
// 批量入库归并在库存信息入口下,仍用独立页签承载预览/入库/撤回,避免挤占库存列表操作区。
$.modal.openTab("轮胎批量入库", prefix + "/batchInbound");
}
/* 用户管理-新增-选择部门树 */
function selectDeptTree() {
var treeId = $("#treeId").val();
@ -186,4 +194,4 @@
}
</script>
</body>
</html>
</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 '<span class="label label-warning">已发货待入库</span>';
}
return $.table.selectDictLabel(inventoryStatusDict, value);
}
},
{
field : 'carNo',

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

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

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

@ -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<InboundBatchPreviewItemVo> 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<InboundBatchPreviewItemVo> getItems()
{
return items;
}
public void setItems(List<InboundBatchPreviewItemVo> items)
{
this.items = items;
}
}

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

@ -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<Map> vTyreStockSummary(SysDept sysDept);
/**
*
*
* @param inboundCode
* @return
*/
List<BaseTyre> selectByInboundCode(@Param("inboundCode") String inboundCode);
/**
*
*
* @param inboundCode
* @return
*/
int countBaseTyreByInboundCode(@Param("inboundCode") String inboundCode);
}

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

@ -75,4 +75,13 @@ public interface IBaseTyreService
String importBaseTyre(List<BaseTyreVo> baseTyreVoList, boolean updateSupport, String operName);
List<Map> vTyreStockSummary(SysDept sysDept);
/**
*
*
* @param inboundCode
* @return
*/
List<BaseTyre> selectBaseTyresByInboundCode(String inboundCode);
}

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

@ -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<BaseTyre> 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<BaseTyre> 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<BaseTyre> 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<BaseTyre> tyres)
{
int exists = 0; // 统计已入库的轮胎条数
List<InboundBatchPreviewItemVo> 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; // 返回预览结果
}
}

@ -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<String> 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<Map> mapList = baseTyreMapper.vTyreStockSummary(sysDept);
return mapList;
}
@Override
public List<BaseTyre> selectBaseTyresByInboundCode(String inboundCode) {
return baseTyreMapper.selectByInboundCode(inboundCode);
}
}

@ -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; // 单条撤回成功
}
}

@ -143,4 +143,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
delete from base_inventory where tyre_rfid = #{tyreRfid}
</delete>
</mapper>
<delete id="deleteInStockBaseInventoryByEpc">
delete from base_inventory
where tyre_rfid = #{tyreRfid}
and status = '0'
</delete>
</mapper>

@ -28,12 +28,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="company" column="company" />
<result property="carTeam" column="carTeam" />
<result property="grooves" column="grooves" />
<result property="inventoryStatus" column="inventory_status" />
</resultMap>
<sql id="selectBaseTyreVo">
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"
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(d.create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
</if>
<if test="inventoryStatus != null and inventoryStatus != ''">
AND d.inventory_status = #{inventoryStatus}
</if>
${params.dataScope}
</select>
@ -160,6 +165,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="remark != null and remark != ''">remark,</if>
<if test="deptId != null and deptId != ''">dept_id,</if>
<if test="patternDepth != null and patternDepth != ''">pattern_depth,</if>
<if test="inventoryStatus != null and inventoryStatus != ''">inventory_status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="tyreId != null ">#{tyreId},</if>
@ -181,6 +187,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="remark != null and remark != ''">#{remark},</if>
<if test="deptId != null and deptId != ''">#{deptId},</if>
<if test="patternDepth != null and patternDepth != ''">#{patternDepth},</if>
<if test="inventoryStatus != null and inventoryStatus != ''">#{inventoryStatus},</if>
</trim>
</insert>
@ -205,6 +212,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="remark != null and remark != ''">remark = #{remark},</if>
<if test="carNo != null ">car_no = #{carNo},</if>
<if test="wheelPostion != null ">wheel_postion = #{wheelPostion},</if>
<if test="inventoryStatus != null">inventory_status = #{inventoryStatus},</if>
</trim>
where tyre_id = #{tyreId}
</update>

Loading…
Cancel
Save