Commit d09efe6812f01d770e7f125f365459da25704d0d

Authored by 易文鹏
1 parent af09cffd

feat:并发问题:如果多个线程同时修改同一个shipmentDetail的状态,可能会导致数据不一致,用redis锁解决

src/main/java/com/huaheng/framework/config/ShiroConfig.java
@@ -131,7 +131,7 @@ public class ShiroConfig { @@ -131,7 +131,7 @@ public class ShiroConfig {
131 public SpringSessionValidationScheduler sessionValidationScheduler() { 131 public SpringSessionValidationScheduler sessionValidationScheduler() {
132 SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler(); 132 SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler();
133 // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟 133 // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
134 - sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 1000); 134 + sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 4000);
135 // 设置会话验证调度器进行会话验证时的会话管理器 135 // 设置会话验证调度器进行会话验证时的会话管理器
136 sessionValidationScheduler.setSessionManager(sessionValidationManager()); 136 sessionValidationScheduler.setSessionManager(sessionValidationManager());
137 return sessionValidationScheduler; 137 return sessionValidationScheduler;
@@ -148,7 +148,7 @@ public class ShiroConfig { @@ -148,7 +148,7 @@ public class ShiroConfig {
148 // 删除过期的session 148 // 删除过期的session
149 manager.setDeleteInvalidSessions(true); 149 manager.setDeleteInvalidSessions(true);
150 // 设置全局session超时时间 150 // 设置全局session超时时间
151 - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); 151 + manager.setGlobalSessionTimeout(expireTime * 60 * 4000);
152 // 去掉 JSESSIONID 152 // 去掉 JSESSIONID
153 manager.setSessionIdUrlRewritingEnabled(false); 153 manager.setSessionIdUrlRewritingEnabled(false);
154 // 是否定时检查session 154 // 是否定时检查session
@@ -171,7 +171,7 @@ public class ShiroConfig { @@ -171,7 +171,7 @@ public class ShiroConfig {
171 // 删除过期的session 171 // 删除过期的session
172 manager.setDeleteInvalidSessions(true); 172 manager.setDeleteInvalidSessions(true);
173 // 设置全局session超时时间 173 // 设置全局session超时时间
174 - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); 174 + manager.setGlobalSessionTimeout(expireTime * 60 * 4000);
175 // 去掉 JSESSIONID 175 // 去掉 JSESSIONID
176 manager.setSessionIdUrlRewritingEnabled(false); 176 manager.setSessionIdUrlRewritingEnabled(false);
177 // 定义要使用的无效的Session定时调度器 177 // 定义要使用的无效的Session定时调度器
@@ -288,7 +288,7 @@ public class ShiroConfig { @@ -288,7 +288,7 @@ public class ShiroConfig {
288 //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon"); 288 //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon");
289 //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon"); 289 //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon");
290 //filterChainDefinitionMap.put("/config/zone/getAllFlatLocation", "anon"); 290 //filterChainDefinitionMap.put("/config/zone/getAllFlatLocation", "anon");
291 - //filterChainDefinitionMap.put("/mobile/", "anon"); 291 + //filterChainDefinitionMap.put("/mobile/getModules2", "anon");
292 292
293 293
294 // 系统权限列表 294 // 系统权限列表
@@ -337,7 +337,7 @@ public class ShiroConfig { @@ -337,7 +337,7 @@ public class ShiroConfig {
337 cookie.setDomain(domain); 337 cookie.setDomain(domain);
338 cookie.setPath(path); 338 cookie.setPath(path);
339 cookie.setHttpOnly(httpOnly); 339 cookie.setHttpOnly(httpOnly);
340 - cookie.setMaxAge(maxAge * 24 * 60 * 60); 340 + cookie.setMaxAge(maxAge * 24 * 60 * 240);
341 return cookie; 341 return cookie;
342 } 342 }
343 343
src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java
@@ -531,6 +531,9 @@ public class ReceiptTaskService { @@ -531,6 +531,9 @@ public class ReceiptTaskService {
531 .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId()); 531 .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId());
532 InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory); 532 InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory);
533 Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode); 533 Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode);
  534 + if (material == null) {
  535 + throw new ServiceException("物料不存在," + receiptDetail.getMaterialCode());
  536 + }
534 /*单位换算*/ 537 /*单位换算*/
535 BigDecimal receiptQty = taskDetail.getQty(); 538 BigDecimal receiptQty = taskDetail.getQty();
536 if (StringUtils.isNotEmpty(receiptDetail.getMaterialUnit()) && StringUtils.isNotEmpty(material.getUnit()) && !receiptDetail.getMaterialUnit().equals(material.getUnit())) { 539 if (StringUtils.isNotEmpty(receiptDetail.getMaterialUnit()) && StringUtils.isNotEmpty(material.getUnit()) && !receiptDetail.getMaterialUnit().equals(material.getUnit())) {
src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java
@@ -46,6 +46,9 @@ import com.huaheng.pc.task.taskHeader.domain.ShipmentTaskCreateModel; @@ -46,6 +46,9 @@ import com.huaheng.pc.task.taskHeader.domain.ShipmentTaskCreateModel;
46 import com.huaheng.pc.task.taskHeader.domain.TaskHeader; 46 import com.huaheng.pc.task.taskHeader.domain.TaskHeader;
47 import org.slf4j.Logger; 47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory; 48 import org.slf4j.LoggerFactory;
  49 +import org.springframework.beans.factory.annotation.Autowired;
  50 +import org.springframework.data.redis.core.RedisTemplate;
  51 +import org.springframework.data.redis.core.ValueOperations;
49 import org.springframework.stereotype.Service; 52 import org.springframework.stereotype.Service;
50 import org.springframework.transaction.annotation.Transactional; 53 import org.springframework.transaction.annotation.Transactional;
51 54
@@ -53,6 +56,9 @@ import javax.annotation.Resource; @@ -53,6 +56,9 @@ import javax.annotation.Resource;
53 import javax.validation.constraints.NotNull; 56 import javax.validation.constraints.NotNull;
54 import java.math.BigDecimal; 57 import java.math.BigDecimal;
55 import java.util.*; 58 import java.util.*;
  59 +import java.util.concurrent.TimeUnit;
  60 +import java.util.function.Function;
  61 +import java.util.stream.Collectors;
56 62
57 /** 63 /**
58 * 入库任务创建和完成 64 * 入库任务创建和完成
@@ -102,6 +108,9 @@ public class ShipmentTaskService { @@ -102,6 +108,9 @@ public class ShipmentTaskService {
102 @Resource 108 @Resource
103 private StationService stationService; 109 private StationService stationService;
104 110
  111 + @Resource
  112 + private RedisTemplate<String, Object> redisTemplate;
  113 +
105 /** 114 /**
106 * 创建出库任务 115 * 创建出库任务
107 * 116 *
@@ -498,46 +507,110 @@ public class ShipmentTaskService { @@ -498,46 +507,110 @@ public class ShipmentTaskService {
498 507
499 //修改出库单状态 508 //修改出库单状态
500 List<TaskDetail> taskDetailList = taskDetailService.findByTaskId(task.getId()); 509 List<TaskDetail> taskDetailList = taskDetailService.findByTaskId(task.getId());
501 - HashSet<Integer> ids = new HashSet<>(); 510 + // 使用 Set 存储 billDetailId 和 shipmentId,避免重复查询
  511 + Set<Integer> billDetailIds = new HashSet<>();
  512 + Set<Integer> shipmentIds = new HashSet<>();
502 for (TaskDetail taskDetail : taskDetailList) { 513 for (TaskDetail taskDetail : taskDetailList) {
503 - ShipmentDetail shipmentDetail = shipmentDetailService.getById(taskDetail.getBillDetailId());  
504 - if (StringUtils.isNotNull(shipmentDetail)) { 514 + billDetailIds.add(taskDetail.getBillDetailId());
  515 + }
  516 +
  517 + // 批量查询出库单明细
  518 + Map<Integer, ShipmentDetail> shipmentDetailMap = shipmentDetailService.listByIds(billDetailIds)
  519 + .stream().collect(Collectors.toMap(ShipmentDetail::getId, Function.identity()));
  520 +
  521 + // 添加 shipmentId 到 shipmentIds 中
  522 + for (TaskDetail taskDetail : taskDetailList) {
  523 + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId());
  524 + if (shipmentDetail != null) {
  525 + shipmentIds.add(shipmentDetail.getShipmentId());
  526 + }
  527 + }
  528 +
  529 + // 批量查询出库单头信息
  530 + Map<Integer, ShipmentHeader> shipmentHeaderMap = shipmentHeaderService.listByIds(shipmentIds)
  531 + .stream().collect(Collectors.toMap(ShipmentHeader::getId, Function.identity()));
  532 +
  533 + for (TaskDetail taskDetail : taskDetailList) {
  534 + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId());
  535 + if (shipmentDetail != null) {
505 if (shipmentDetail.getQty().compareTo(shipmentDetail.getTaskQty()) == 0) { 536 if (shipmentDetail.getQty().compareTo(shipmentDetail.getTaskQty()) == 0) {
506 - //一条单据明细可能有多条组盘多条任务  
507 - List<ShipmentContainerDetail> list = shipmentContainerDetailService.list(new LambdaQueryWrapper<ShipmentContainerDetail>()  
508 - .eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId()));  
509 - boolean flag = true;  
510 - for (ShipmentContainerDetail shipmentContainerDetail : list) {  
511 - if (shipmentContainerDetail.getStatus() != 20) {  
512 - flag = false;  
513 - break;  
514 - }  
515 - }  
516 - if (flag) { 537 + // 一条单据明细,可能有多条组盘多条任务
  538 + List<ShipmentContainerDetail> list = shipmentContainerDetailService.list(
  539 + new LambdaQueryWrapper<ShipmentContainerDetail>().eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId()));
  540 + boolean allCompleted = list.stream().allMatch(detail -> detail.getStatus() == 20);
  541 +
  542 + if (allCompleted) {
517 shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); 543 shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED);
518 } else { 544 } else {
519 shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_GROUPDISK); 545 shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_GROUPDISK);
520 } 546 }
521 - shipmentDetailService.updateById(shipmentDetail); 547 +
  548 + // 使用 Redis 处理并发更新 shipmentDetail
  549 + updateShipmentDetailWithRedis(shipmentDetail);
  550 + }
  551 + // 更新出库单头信息
  552 + updateShipmentHeaderStatus(shipmentDetail.getShipmentId(), shipmentHeaderMap);
  553 + }
  554 + }
  555 + //删除自建单据物料
  556 + for (String materialCode : materialCodes) {
  557 + List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode));
  558 + if (list.isEmpty()) {
  559 + Material material = materialService.getMaterialByCode(materialCode, "CS0001");
  560 + if (material != null && material.getSelfCreated()) {
  561 + materialService.removeById(material);
522 } 562 }
523 - ids.add(shipmentDetail.getShipmentId());  
524 } 563 }
525 564
526 } 565 }
527 566
528 - /* 更新出库单状态 */  
529 - for (Integer id : ids) {  
530 - ShipmentHeader shipmentHeader = shipmentHeaderService.getById(id);  
531 - if (shipmentHeader != null) { 567 + return AjaxResult.success("完成出库任务成功");
  568 + }
  569 +
  570 + // 使用 Redis 处理并发更新 shipmentDetail
  571 + private void updateShipmentDetailWithRedis(ShipmentDetail shipmentDetail) {
  572 + String lockKey = "shipmentDetailLock:" + shipmentDetail.getId(); // Redis 锁的 key
  573 + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  574 + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
  575 +
  576 + if (lockAcquired != null && lockAcquired) {
  577 + try {
  578 + // 获取锁成功,进行更新操作
  579 + shipmentDetailService.updateById(shipmentDetail); // 直接更新数据库
  580 + } finally {
  581 + redisTemplate.delete(lockKey); // 释放锁
  582 + }
  583 + } else {
  584 + // 获取锁失败,可以考虑重试或其他处理
  585 + throw new ServiceException("更新出库单明细失败,请稍后再试");
  586 + }
  587 + }
  588 +
  589 + // 更新出库单头信息
  590 + private void updateShipmentHeaderStatus(Integer shipmentId, Map<Integer, ShipmentHeader> shipmentHeaderMap) {
  591 + // 构造 Redis 锁的 key
  592 + String lockKey = "shipmentHeaderLock:" + shipmentId;
  593 + // 获取 Redis 操作对象
  594 + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  595 +
  596 + // 尝试获取锁
  597 + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
  598 +
  599 + if (lockAcquired != null && lockAcquired) {
  600 + try {
  601 + ShipmentHeader shipmentHeader = shipmentHeaderMap.get(shipmentId);
  602 + if (shipmentHeader == null) {
  603 + return;
  604 + }
  605 +
  606 + // 获取状态信息并更新出库单头逻辑
532 Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId()); 607 Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId());
533 Integer maxStatus = status.get("maxStatus"); 608 Integer maxStatus = status.get("maxStatus");
534 Integer minStatus = status.get("minStatus"); 609 Integer minStatus = status.get("minStatus");
535 610
536 // 检查 maxStatus 和 minStatus 是否不为空 611 // 检查 maxStatus 和 minStatus 是否不为空
537 if (maxStatus != null && minStatus != null) { 612 if (maxStatus != null && minStatus != null) {
538 - boolean isStatusCompleted = QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus);  
539 -  
540 - if (isStatusCompleted) { 613 + if (QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus)) {
541 shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); 614 shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED);
542 } 615 }
543 616
@@ -548,26 +621,22 @@ public class ShipmentTaskService { @@ -548,26 +621,22 @@ public class ShipmentTaskService {
548 } 621 }
549 } 622 }
550 shipmentHeader.setLastUpdated(new Date()); 623 shipmentHeader.setLastUpdated(new Date());
  624 +
  625 + // 使用乐观锁更新出库单头信息
551 if (!shipmentHeaderService.updateById(shipmentHeader)) { 626 if (!shipmentHeaderService.updateById(shipmentHeader)) {
552 - throw new ServiceException("更新库单头表失败"); 627 + throw new ServiceException("更新库单头表失败");
553 } 628 }
  629 + } finally {
  630 + // 释放锁
  631 + redisTemplate.delete(lockKey);
554 } 632 }
  633 + } else {
  634 + // 获取锁失败,可以考虑重试或者其他处理方式
  635 + throw new ServiceException("获取锁失败,请稍后再试");
555 } 636 }
556 - //删除自建单据物料  
557 - for (String materialCode : materialCodes) {  
558 - List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode));  
559 - if (list.isEmpty()) {  
560 - Material material = materialService.getMaterialByCode(materialCode, "CS0001");  
561 - if (material != null && material.getSelfCreated()) {  
562 - materialService.removeById(material);  
563 - }  
564 - }  
565 -  
566 - }  
567 -  
568 - return AjaxResult.success("完成出库任务成功");  
569 } 637 }
570 638
  639 +
571 /** 640 /**
572 * 出库任务完成时更新库位和容器状态 641 * 出库任务完成时更新库位和容器状态
573 * 642 *