feat(wms): 添加钢卷回滚功能替代原有的复活功能

- 将原有的reviveCoil方法重命名为rollbackCoil,实现更精确的回滚操作
- 修改接口路径从GET /reviveCoil/{coilId}改为POST /rollback/{currentCoilId}
- 新增详细的回滚验证逻辑,包括检查数据类型、删除标志和发货状态
- 实现基于二维码记录的钢卷历史追踪,支持撤销更新、合卷和分卷操作
- 添加回滚操作的阻止机制,防止对初始创建、合卷产物和分卷产物进行回滚
- 完善回滚过程中的库位释放和数据恢复流程
- 更新二维码记录以记录回滚操作步骤和相关信息
This commit is contained in:
2026-01-20 09:57:59 +08:00
parent 567a1d5cae
commit 1f284d3607
3 changed files with 335 additions and 75 deletions

View File

@@ -301,30 +301,28 @@ public class WmsMaterialCoilController extends BaseController {
}
/**
* 复活历史钢卷
* 将dataType=0的历史钢卷恢复为dataType=1的当前钢卷
* 同时清空实际库位绑定,并检查当前钢卷号是否重复
* 回滚钢卷操作
* 根据当前钢卷ID找到历史钢卷恢复,删除当前钢卷
* 用于撤销单个更新、合卷或分卷操作
*
* @param coilId 钢卷ID
* @return 复活结果,包含success状态、错误信息、钢卷ID和当前钢卷号
* @param currentCoilId 当前钢卷ID(需要回滚的钢卷)
* @return 操作结果,包含恢复的历史钢卷信息
*/
@Log(title = "钢卷物料表", businessType = BusinessType.UPDATE)
@GetMapping("/reviveCoil/{coilId}")
public R<Map<String, Object>> reviveCoil(
@NotNull(message = "钢卷ID不能为空")
@PathVariable("coilId") Long coilId) {
@RepeatSubmit()
@PostMapping("/rollback/{currentCoilId}")
public R<Map<String, Object>> rollbackCoil(
@NotNull(message = "当前钢卷ID不能为空")
@PathVariable("currentCoilId") Long currentCoilId) {
try {
Map<String, Object> result = iWmsMaterialCoilService.reviveCoil(coilId);
// 根据业务结果返回成功/失败的统一响应
if (Boolean.TRUE.equals(result.get("success"))) {
return R.ok(result);
} else {
return R.fail(result.get("message").toString(), result);
}
} catch (Exception e) {
return R.fail("复活钢卷失败:" + e.getMessage());
Map<String, Object> rollbackResult = iWmsMaterialCoilService.rollbackCoil(currentCoilId);
return R.ok(rollbackResult);
} catch (RuntimeException e) {
return R.fail("回滚失败:" + e.getMessage());
}
}
}

View File

@@ -150,13 +150,14 @@ public interface IWmsMaterialCoilService {
Map<String, Object> getDuplicateCoilGroups();
/**
* 复活历史钢卷
* 将dataType=0的历史钢卷恢复为dataType=1的当前钢卷
* 同时清空实际库位绑定,并检查当前钢卷号是否重复
* 回滚钢卷操作
* 根据当前钢卷ID找到历史钢卷恢复,删除当前钢卷
* 用于撤销单个更新、合卷或分卷操作
*
* @param coilId 钢卷ID
* @return 复活结果,包含success状态和错误信息
* @param currentCoilId 当前钢卷ID(需要回滚的钢卷)
* @return 操作结果,包含恢复的历史钢卷信息
*/
Map<String, Object> reviveCoil(@NotNull(message = "钢卷ID不能为空") Long coilId);
Map<String, Object> rollbackCoil(@NotNull(message = "当前钢卷ID不能为空") Long currentCoilId);
}

View File

@@ -2754,72 +2754,333 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
/**
* 复活历史钢卷
* 将dataType=0的历史钢卷恢复为dataType=1的当前钢卷
* 同时清空实际库位绑定,并检查当前钢卷号是否重复
* 回滚钢卷操作
* 根据当前钢卷ID找到历史钢卷恢复,删除当前钢卷
* 用于撤销单个更新、合卷或分卷操作
*
* @param currentCoilId 当前钢卷ID需要回滚的钢卷
* @return 操作结果,包含恢复的历史钢卷信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> reviveCoil(Long coilId) {
public Map<String, Object> rollbackCoil(Long currentCoilId) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
// 1. 查询钢卷信息
WmsMaterialCoil coil = baseMapper.selectById(coilId);
if (coil == null) {
result.put("message", "钢卷不存在");
return result;
}
// 2. 检查是否为历史数据
if (coil.getDataType() != 0) {
result.put("message", "该钢卷不是历史数据,无需复活");
return result;
}
// 3. 检查当前钢卷号是否重复(现存且未逻辑删除的)
if (StringUtils.isNotBlank(coil.getCurrentCoilNo())) {
LambdaQueryWrapper<WmsMaterialCoil> duplicateCheck = Wrappers.lambdaQuery();
duplicateCheck.eq(WmsMaterialCoil::getCurrentCoilNo, coil.getCurrentCoilNo())
.eq(WmsMaterialCoil::getDelFlag, 0)
.eq(WmsMaterialCoil::getDataType, 1) // 只检查当前数据
.ne(WmsMaterialCoil::getCoilId, coilId); // 排除自身
long duplicateCount = baseMapper.selectCount(duplicateCheck);
if (duplicateCount > 0) {
result.put("message", "当前钢卷号[" + coil.getCurrentCoilNo() + "]已存在重复记录,请先删除错误钢卷后再复活");
result.put("duplicateCoilNo", coil.getCurrentCoilNo());
return result;
}
}
// 4. 执行复活操作
try {
// 记录原实际库位ID用于后续释放
Long oldActualWarehouseId = coil.getActualWarehouseId();
// 更新钢卷状态dataType=1actualWarehouseId=null
LambdaUpdateWrapper<WmsMaterialCoil> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(WmsMaterialCoil::getCoilId, coilId)
.set(WmsMaterialCoil::getDataType, 1) // 恢复为当前数据
.set(WmsMaterialCoil::getActualWarehouseId, (Long) null); // 清空实际库位绑定
int updateCount = baseMapper.update(null, updateWrapper);
if (updateCount <= 0) {
throw new RuntimeException("复活钢卷失败:数据库更新异常");
// 1. 查询当前钢卷信息
WmsMaterialCoil currentCoil = baseMapper.selectById(currentCoilId);
if (currentCoil == null) {
throw new RuntimeException("当前钢卷不存在");
}
// 2. 检查当前钢卷是否为当前数据(不能回滚历史数据)
if (currentCoil.getDataType() != 1) {
throw new RuntimeException("只能回滚当前数据,不能回滚历史数据");
}
if (!currentCoil.getDelFlag().equals(2)) {
throw new RuntimeException("当前钢卷已删除,无法回滚");
}
if (currentCoil.getStatus() == 1) {
throw new RuntimeException("只能回滚未发货数据,不能回滚已发货数据");
}
// 3. 检查是否有二维码记录
if (currentCoil.getQrcodeRecordId() == null) {
throw new RuntimeException("当前钢卷没有二维码记录,无法回滚");
}
// 4. 查询二维码记录并解析上一步钢卷ID
WmsGenerateRecordVo qrcodeRecord = generateRecordService.queryById(currentCoil.getQrcodeRecordId());
if (qrcodeRecord == null || StringUtils.isBlank(qrcodeRecord.getContent())) {
throw new RuntimeException("二维码记录不存在或内容为空,无法回滚");
}
// 5. 解析二维码内容找到上一步的历史钢卷ID
Long historyCoilId = parseHistoryCoilIdFromQrcode(qrcodeRecord.getContent(), currentCoilId);
if (historyCoilId == null) {
// 检查无法回滚的具体原因
String rollbackBlockReason = getRollbackBlockReason(qrcodeRecord.getContent(), currentCoilId);
switch (rollbackBlockReason) {
case "INITIAL_CREATE":
throw new RuntimeException("无法继续回滚:当前钢卷为最初的新增操作创建,如需删除请直接使用删除功能");
case "MERGE_PRODUCT":
throw new RuntimeException("无法回滚:当前钢卷是合卷操作的产物,回滚会导致其他相关钢卷数据异常");
case "SPLIT_PRODUCT":
throw new RuntimeException("无法回滚:当前钢卷是分卷操作的产物,回滚会导致其他相关钢卷数据异常");
default:
throw new RuntimeException("无法从二维码记录中找到历史钢卷ID无法回滚");
}
}
// 6. 查询历史钢卷信息
WmsMaterialCoil historyCoil = baseMapper.selectById(historyCoilId);
if (historyCoil == null) {
throw new RuntimeException("历史钢卷不存在,无法回滚");
}
// 7. 检查历史钢卷是否为历史数据
if (historyCoil.getDataType() != 0) {
throw new RuntimeException("目标钢卷不是历史数据,无法恢复");
}
// 8. 检查历史钢卷的钢卷号是否重复
Map<String, Object> duplicateCheck = checkCoilNoDuplicate(null, historyCoil.getEnterCoilNo(), historyCoil.getCurrentCoilNo());
String duplicateType = (String) duplicateCheck.get("duplicateType");
if (!"none".equals(duplicateType)) {
String errorMsg = "无法恢复历史钢卷,存在重复的钢卷号:";
if ("enter".equals(duplicateType)) {
errorMsg += "入场钢卷号[" + historyCoil.getEnterCoilNo() + "]重复";
} else if ("current".equals(duplicateType)) {
errorMsg += "当前钢卷号[" + historyCoil.getCurrentCoilNo() + "]重复";
} else if ("both".equals(duplicateType)) {
errorMsg += "入场钢卷号[" + historyCoil.getEnterCoilNo() + "]和当前钢卷号[" + historyCoil.getCurrentCoilNo() + "]都重复";
}
errorMsg += "。请先删除重复的钢卷后再进行回滚操作。";
throw new RuntimeException(errorMsg);
}
// 9. 执行回滚操作
// 9.1 释放当前钢卷占用的实际库区
if (currentCoil.getActualWarehouseId() != null) {
WmsActualWarehouseBo releaseBo = new WmsActualWarehouseBo();
releaseBo.setActualWarehouseId(currentCoil.getActualWarehouseId());
releaseBo.setIsEnabled(1); // 设置为启用状态
actualWarehouseService.updateByBo(releaseBo);
}
// 9.2 删除当前钢卷
baseMapper.deleteById(currentCoilId);
// 9.3 恢复历史钢卷为当前数据
LambdaUpdateWrapper<WmsMaterialCoil> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(WmsMaterialCoil::getCoilId, historyCoilId)
.set(WmsMaterialCoil::getDataType, 1) // 恢复为当前数据
.set(WmsMaterialCoil::getActualWarehouseId, (Long) null); // 清空实际库区绑定
baseMapper.update(null, updateWrapper);
// 9.4 更新二维码记录,添加回滚步骤并更新相关字段
updateQrcodeForRollback(qrcodeRecord, currentCoil, historyCoil);
// 10. 重新查询恢复后的历史钢卷信息
WmsMaterialCoilVo restoredCoil = queryById(historyCoilId);
// 11. 返回操作结果
result.put("success", true);
result.put("message", "钢卷复活成功");
result.put("coilId", coilId);
result.put("currentCoilNo", coil.getCurrentCoilNo());
result.put("restoredCoil", restoredCoil);
result.put("deletedCoilId", currentCoilId);
result.put("message", "回滚操作成功,已恢复历史钢卷,删除了当前钢卷");
log.info("钢卷回滚操作成功历史钢卷ID={}, 删除的钢卷ID={}", historyCoilId, currentCoilId);
} catch (Exception e) {
log.error("复活钢卷失败coilId: {}", coilId, e);
throw new RuntimeException("复活钢卷失败: " + e.getMessage());
log.error("钢卷回滚操作失败currentCoilId={}", currentCoilId, e);
result.put("success", false);
result.put("message", "回滚操作失败:" + e.getMessage());
throw e; // 重新抛出异常,让事务回滚
}
return result;
}
/**
* 从二维码内容中解析历史钢卷ID
* 根据当前钢卷ID找到对应的上一步操作中的old_coil_id
*/
private Long parseHistoryCoilIdFromQrcode(String content, Long currentCoilId) {
try {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> contentMap = objectMapper.readValue(content, Map.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
if (steps == null || steps.isEmpty()) {
return null;
}
String currentCoilIdStr = currentCoilId.toString();
// 从后往前查找找到当前钢卷ID对应的上一步操作
for (int i = steps.size() - 1; i >= 0; i--) {
Map<String, Object> step = steps.get(i);
String action = (String) step.get("action");
// 如果是新增操作,说明已经回滚到最初状态
if ("新增".equals(action)) {
return null; // 无法继续回滚
}
// 如果是回滚操作,跳过(回滚步骤不应该影响历史查找)
if ("回滚".equals(action)) {
continue;
}
// 检查这个步骤是否产生了当前钢卷
Object newCoilIdObj = step.get("new_coil_id");
if (newCoilIdObj != null) {
String newCoilIdStr = newCoilIdObj.toString();
if (newCoilIdStr.equals(currentCoilIdStr)) {
// 找到了产生当前钢卷的步骤获取old_coil_id
Object oldCoilIdObj = step.get("old_coil_id");
if (oldCoilIdObj != null) {
String oldCoilIdStr = oldCoilIdObj.toString();
if (!"null".equals(oldCoilIdStr) && !oldCoilIdStr.isEmpty()) {
try {
return Long.parseLong(oldCoilIdStr);
} catch (NumberFormatException e) {
log.warn("解析old_coil_id失败{}", oldCoilIdStr);
}
}
}
break;
}
}
// 对于合卷操作检查parent_coil_ids
if ("合卷".equals(step.get("operation"))) {
Object parentCoilIdsObj = step.get("parent_coil_ids");
if (parentCoilIdsObj != null) {
String parentCoilIdsStr = parentCoilIdsObj.toString();
if (parentCoilIdsStr.contains(currentCoilIdStr)) {
// 当前钢卷是合卷的产物返回null表示无法继续回滚
return null;
}
}
}
// 对于分卷操作检查new_current_coil_nos是否包含多个钢卷号
if ("分卷".equals(step.get("operation"))) {
// 当前钢卷是分卷操作产生的子钢卷,无法回滚
// 分卷操作会产生多个钢卷,回滚其中一个会导致其他钢卷异常
return null;
}
}
} catch (Exception e) {
log.error("解析二维码内容失败", e);
}
return null;
}
/**
* 获取无法回滚的具体原因
* @return "INITIAL_CREATE" - 初始新增操作
* "MERGE_PRODUCT" - 合卷产物
* "SPLIT_PRODUCT" - 分卷产物
* "UNKNOWN" - 其他未知原因
*/
private String getRollbackBlockReason(String content, Long currentCoilId) {
try {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> contentMap = objectMapper.readValue(content, Map.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
if (steps == null || steps.isEmpty()) {
return "UNKNOWN";
}
String currentCoilIdStr = currentCoilId.toString();
// 首先检查是否为初始新增操作
Map<String, Object> firstStep = steps.get(0);
String action = (String) firstStep.get("action");
if ("新增".equals(action)) {
Object coilIdObj = contentMap.get("coil_id");
Object currentCoilIdObj = contentMap.get("current_coil_id");
if (coilIdObj != null && currentCoilIdObj != null) {
String coilIdStr = coilIdObj.toString();
String currentCoilIdStrInContent = currentCoilIdObj.toString();
if (coilIdStr.equals(currentCoilIdStrInContent) && coilIdStr.equals(currentCoilIdStr)) {
return "INITIAL_CREATE";
}
}
}
// 检查是否为合卷或分卷的产物
// 2. 从后往前遍历steps最新操作优先检查合卷/分卷产物
// 倒序遍历,确保取到最新的操作状态
for (int i = steps.size() - 1; i >= 0; i--) {
Map<String, Object> step = steps.get(i);
String operation = (String) step.get("operation");
// 检查合卷操作(优先判断,因为合卷可能是后续操作)
if ("合卷".equals(operation)) {
Object parentCoilIdsObj = step.get("parent_coil_ids");
if (parentCoilIdsObj != null) {
String parentCoilIdsStr = parentCoilIdsObj.toString();
// 校验当前钢卷ID是否在合卷的父卷列表中
if (parentCoilIdsStr.contains(currentCoilIdStr)) {
return "MERGE_PRODUCT";
}
}
}
// 检查分卷操作
if ("分卷".equals(operation)) {
return "SPLIT_PRODUCT";
}
}
return "UNKNOWN";
} catch (Exception e) {
log.error("检查回滚阻止原因失败", e);
return "UNKNOWN";
}
}
/**
* 更新二维码记录以记录回滚操作
*/
private void updateQrcodeForRollback(WmsGenerateRecordVo qrcodeRecord, WmsMaterialCoil currentCoil, WmsMaterialCoil historyCoil) {
try {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> contentMap = objectMapper.readValue(qrcodeRecord.getContent(), Map.class);
// 获取现有steps
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
if (steps == null) {
steps = new ArrayList<>();
}
// 添加回滚步骤
Map<String, Object> rollbackStep = new HashMap<>();
rollbackStep.put("step", steps.size() + 1);
rollbackStep.put("action", "回滚");
rollbackStep.put("operation", "撤销操作");
rollbackStep.put("deleted_coil_id", String.valueOf(currentCoil.getCoilId()));
rollbackStep.put("restored_coil_id", String.valueOf(historyCoil.getCoilId()));
rollbackStep.put("deleted_current_coil_no", currentCoil.getCurrentCoilNo());
rollbackStep.put("restored_current_coil_no", historyCoil.getCurrentCoilNo());
rollbackStep.put("operator", LoginHelper.getUsername());
rollbackStep.put("rollback_time", new java.util.Date());
steps.add(rollbackStep);
contentMap.put("steps", steps);
// 更新二维码中的当前钢卷信息指向恢复的历史钢卷
contentMap.put("current_coil_id", String.valueOf(historyCoil.getCoilId()));
contentMap.put("current_coil_no", historyCoil.getCurrentCoilNo());
// enter_coil_no 保持不变
// 更新二维码记录
String newContentJson = objectMapper.writeValueAsString(contentMap);
WmsGenerateRecordBo updateBo = new WmsGenerateRecordBo();
updateBo.setRecordId(qrcodeRecord.getRecordId());
updateBo.setContent(newContentJson);
generateRecordService.updateByBo(updateBo);
} catch (Exception e) {
log.error("更新二维码记录失败", e);
throw new RuntimeException("更新二维码记录失败: " + e.getMessage());
}
}
}