This commit is contained in:
2026-10-20 23:49:55 +08:00
parent 35a1b835fe
commit af5cf1e3bd
37 changed files with 714 additions and 184 deletions
@@ -24,6 +24,8 @@ import java.util.List;
@Service
public class DomesticContainerService {
@jakarta.annotation.Resource private com.baiyue.warehouse.service.WarehouseDocumentNumberGenerator documentNumbers;
private static final long PLATFORM_TENANT = 0L;
@Resource private DomesticContainerRepository repository;
@Resource private WarehouseRepository warehouseRepository;
@@ -57,7 +59,7 @@ public class DomesticContainerService {
String container = normalize(request.containerNo());
DomesticContainerOrderEntity order = new DomesticContainerOrderEntity();
order.setId(IdWorker.getId()); order.setTenantId(PLATFORM_TENANT); order.setBusinessType("INBOUND");
order.setOrderNo(nextNo("ZGRK")); order.setWarehouseId(warehouse.getId()); order.setWarehouseName(warehouse.getWarehouseName());
order.setOrderNo(documentNumbers.next("ZGRK")); order.setWarehouseId(warehouse.getId()); order.setWarehouseName(warehouse.getWarehouseName());
order.setContainerNo(container); order.setCargoName(request.cargoName().trim()); order.setBoxCount(request.boxCount());
order.setVolumeCbm(request.volumeCbm()); order.setOperationTime(request.inboundTime()); order.setStatus(request.status()); order.setRemark(request.remark());
if ("COMPLETED".equals(request.status())) {
@@ -86,7 +88,7 @@ public class DomesticContainerService {
if (request.boxCount() > inventory.getAvailableBoxes()) throw new BizException(ResultCode.CONFLICT, "出库箱数超过可用库存");
}
DomesticContainerOrderEntity order = new DomesticContainerOrderEntity(); order.setId(IdWorker.getId()); order.setTenantId(PLATFORM_TENANT); order.setBusinessType("OUTBOUND");
order.setOrderNo(nextNo("ZGCK")); order.setWarehouseId(warehouse.getId()); order.setWarehouseName(warehouse.getWarehouseName()); order.setContainerNo(inventory.getContainerNo());
order.setOrderNo(documentNumbers.next("ZGCK")); order.setWarehouseId(warehouse.getId()); order.setWarehouseName(warehouse.getWarehouseName()); order.setContainerNo(inventory.getContainerNo());
order.setCargoName(inventory.getCargoName()); order.setBoxCount(request.boxCount()); order.setVolumeCbm(BigDecimal.ZERO); order.setOperationTime(request.outboundTime()); order.setStatus(request.status()); order.setRemark(request.remark());
repository.insertOrder(order);
if ("COMPLETED".equals(request.status())) { inventory.setOutboundBoxes(inventory.getOutboundBoxes()+order.getBoxCount()); inventory.setAvailableBoxes(inventory.getAvailableBoxes()-order.getBoxCount()); inventory.setInventoryStatus(status(inventory)); requireUpdated(repository.updateInventory(inventory)); transaction(inventory, order, "OUTBOUND"); }
@@ -107,7 +109,6 @@ public class DomesticContainerService {
private void requireUpdated(int count) { if (count != 1) throw new BizException(ResultCode.CONFLICT, "库存已被并发修改,请刷新后重试"); }
private void requireClaimCompleted(int count) { if (count != 1) throw new BizException(ResultCode.CONFLICT, "幂等操作状态已变化,请使用原键重试"); }
private DomesticContainerDtos.CreateResult claim(String type, String key, String hash) { try { if (repository.claim(IdWorker.getId(), type, key, hash) == 1) return null; } catch (DuplicateKeyException ignored) {} DomesticContainerIdempotencyEntity e = repository.findClaim(type, key); if (e == null || !hash.equals(e.getRequestHash())) throw new BizException(ResultCode.CONFLICT, "幂等键已用于其他请求"); if ("COMPLETED".equals(e.getOperationStatus())) return new DomesticContainerDtos.CreateResult(e.getResourceId(), e.getResourceNo(), true); throw new BizException(ResultCode.CONFLICT, "幂等请求正在处理中"); }
private String nextNo(String prefix) { return prefix + java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now()) + IdWorker.getIdStr().substring(10); }
private static String normalize(String s) { return s.trim().toUpperCase(java.util.Locale.ROOT); }
private static String hash(String value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new IllegalStateException(e); } }
private PageResult<DomesticContainerDtos.OrderView> mapOrders(PageResult<DomesticContainerOrderEntity> p) { return new PageResult<>(p.records().stream().map(o -> new DomesticContainerDtos.OrderView(o.getId(),o.getBusinessType(),o.getOrderNo(),o.getWarehouseId(),o.getWarehouseName(),o.getContainerNo(),o.getCargoName(),o.getBoxCount(),o.getVolumeCbm(),o.getOperationTime(),o.getStatus(),o.getRemark(),o.getVersion(),o.getCreatedAt())).toList(),p.page(),p.size(),p.total(),p.pages()); }
@@ -0,0 +1,21 @@
package com.baiyue.warehouse.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** 本服务业务编号序列,更新与读取必须处于同一事务连接。 */
@Mapper
public interface DocumentSequenceMapper {
/** 唯一序列键原子递增,并在连接内保存本次分配值。 */
@Insert("INSERT INTO wh_document_sequence(sequence_key,current_value) VALUES(#{key},LAST_INSERT_ID(1)) "
+ "ON DUPLICATE KEY UPDATE current_value=LAST_INSERT_ID(current_value+1)")
int allocate(@Param("key") String key);
/** 禁用一级和二级缓存,返回本事务刚刚分配的值。 */
@Select("SELECT LAST_INSERT_ID()")
@Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE)
Long allocatedValue();
}
@@ -0,0 +1,24 @@
package com.baiyue.warehouse.repository;
import com.baiyue.warehouse.mapper.DocumentSequenceMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/** 各业务共用本库序列,独立短事务避免持有编号锁进入业务资源锁。 */
@Repository
public class DocumentSequenceRepository {
private final DocumentSequenceMapper mapper;
/** 只访问所属服务的编号表。 */
public DocumentSequenceRepository(DocumentSequenceMapper mapper) { this.mapper = mapper; }
/** 已分配号不因外层业务回滚而复用,允许失败请求产生断号。 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public long next(String key) {
mapper.allocate(key);
Long value = mapper.allocatedValue();
if (value == null || value < 1) throw new IllegalStateException("业务编号分配失败");
return value;
}
}
@@ -0,0 +1,34 @@
package com.baiyue.warehouse.service;
import com.baiyue.common.core.number.DailyDocumentNumber;
import com.baiyue.warehouse.repository.DocumentSequenceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.util.Set;
/** 按北京时间生成两至四位前缀、八位日期和四位日流水。 */
@Component
public class WarehouseDocumentNumberGenerator {
private static final Set<String> PREFIXES = Set.of("ZGRK", "ZGCK");
private final DocumentSequenceRepository sequences;
private final Clock clock;
/** 正式运行使用统一业务时区。 */
@Autowired
public WarehouseDocumentNumberGenerator(DocumentSequenceRepository sequences) {
this(sequences, Clock.system(DailyDocumentNumber.BUSINESS_ZONE));
}
/** 固定时钟用于跨日及前缀独立性验证。 */
WarehouseDocumentNumberGenerator(DocumentSequenceRepository sequences, Clock clock) {
this.sequences = sequences;
this.clock = clock;
}
/** 只为本服务已登记的业务类型分配号码。 */
public String next(String prefix) {
if (!PREFIXES.contains(prefix)) throw new IllegalArgumentException("未登记的业务编号前缀:" + prefix);
return DailyDocumentNumber.next(prefix, clock, sequences::next);
}
}