优化
This commit is contained in:
+4
-10
@@ -29,6 +29,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ContainerCustomsService {
|
||||
@jakarta.annotation.Resource private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
public static final String WAITING = "WAITING_CUSTOMER_DOCUMENTS";
|
||||
public static final String READY = "READY_TO_START";
|
||||
public static final String DECLARING = "DECLARING";
|
||||
@@ -197,7 +199,7 @@ public class ContainerCustomsService {
|
||||
throw conflict("报关资料尚未全部冻结,不能发送给报关行");
|
||||
}
|
||||
long submissionId = IdWorker.getId();
|
||||
String batchNo = brokerBatchNo(planId);
|
||||
String batchNo = documentNumbers.next("BR");
|
||||
String snapshot = brokerSubmissionSnapshot(container.id());
|
||||
if (mapper.insertBrokerSubmission(submissionId, container.id(), planId, batchNo, container.version(), snapshot,
|
||||
operatorId, operatorName) != 1) throw conflict("首次发送报关行批次创建失败");
|
||||
@@ -372,7 +374,7 @@ public class ContainerCustomsService {
|
||||
if (!Objects.equals(container.version(), request.expectedVersion())) throw concurrent();
|
||||
long batchId = IdWorker.getId();
|
||||
String batchNo = trim(request.batchNo());
|
||||
if (batchNo == null) batchNo = "DECL-" + batchId;
|
||||
if (batchNo == null) batchNo = documentNumbers.next("DECL");
|
||||
String snapshot = declarationSnapshot(container.id());
|
||||
if (mapper.insertDeclarationBatch(batchId, container.id(), planId, batchNo, container.version(), snapshot,
|
||||
trim(request.remark())) != 1) throw conflict("正式申报批次创建失败");
|
||||
@@ -501,14 +503,6 @@ public class ContainerCustomsService {
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException(ex); }
|
||||
}
|
||||
|
||||
private String brokerBatchNo(long planId) {
|
||||
PlanRow plan = requiredPlan(planId);
|
||||
String planNo = trim(plan.planNo());
|
||||
if (planNo != null && planNo.matches("[A-Za-z0-9_-]{1,52}")) return "BR-" + planNo + "-01";
|
||||
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(Locale.ROOT);
|
||||
return "BR-" + LocalDate.now() + "-" + suffix;
|
||||
}
|
||||
|
||||
private String brokerSubmissionSnapshot(long containerId) {
|
||||
List<Map<String,Object>> snapshot = mapper.documents(containerId, null).stream().map(document -> {
|
||||
List<ScopeRow> scopes = mapper.frozenScopes(document.id());
|
||||
|
||||
+3
-1
@@ -38,6 +38,8 @@ import java.util.Set;
|
||||
/** 同柜不可拆组的唯一状态机与平台编排服务。 */
|
||||
@Service
|
||||
public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
@jakarta.annotation.Resource private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private static final Set<String> CURRENT_GROUP_STATUSES = Set.of("ACTIVE", "LOCKED", "CONSUMED");
|
||||
private static final Set<String> PRE_LOADING_GROUP_STATUSES = Set.of(
|
||||
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
@@ -64,7 +66,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
validateSameRoute(candidates);
|
||||
Long lockedPlanId = validateAllocationState(candidates);
|
||||
long groupId = IdWorker.getId();
|
||||
String groupNo = "CLG-" + groupId;
|
||||
String groupNo = documentNumbers.next("CLG");
|
||||
InboundCandidateRow first = candidates.get(0);
|
||||
mapper.insertGroup(groupId, groupNo, first.getWarehouseId(), first.getChannelCode().trim(),
|
||||
operatorId, trim(operatorName));
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.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 fl_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();
|
||||
}
|
||||
+3
-1
@@ -32,6 +32,8 @@ import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class PortScheduleService {
|
||||
@jakarta.annotation.Resource private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private static final Set<String> SOURCE_TYPES = Set.of("MANUAL", "CARRIER", "TERMINAL", "API");
|
||||
|
||||
@Resource private PortScheduleRepository repository;
|
||||
@@ -72,7 +74,7 @@ public class PortScheduleService {
|
||||
validateLinks(request.data());
|
||||
PortScheduleEntity value = new PortScheduleEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setScheduleNo("PS" + value.getId());
|
||||
value.setScheduleNo(documentNumbers.next("PS"));
|
||||
copy(value, request.data());
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.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;
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -40,6 +40,8 @@ import static com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.*;
|
||||
|
||||
@Service
|
||||
public class SailingCalendarService {
|
||||
@jakarta.annotation.Resource private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final String OPEN = "OPEN";
|
||||
@Resource private SailingCalendarRepository repository;
|
||||
@@ -77,7 +79,7 @@ public class SailingCalendarService {
|
||||
if (version(request.expectedVersion()) != 0) throw bad("新建模板expectedVersion必须为0");
|
||||
SailingChannelEntity channel = resolvePriceBookChannel(request.data().channelId(), request.data().warehouseId(), null); validateTemplate(request.data(), channel);
|
||||
if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能创建模板");
|
||||
SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(IdWorker.getId()); value.setTemplateNo("ST-" + value.getId());
|
||||
SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(IdWorker.getId()); value.setTemplateNo(documentNumbers.next("ST"));
|
||||
copy(value, request.data(), channel); value.setStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); now(value);
|
||||
repository.insert(value); return value;
|
||||
@@ -310,7 +312,7 @@ public class SailingCalendarService {
|
||||
if (existing != null) return isCopyOf(existing, copyReason) ? detail(existing.getId())
|
||||
: throwCopyConflict(request.etdAt(), source.getVoyageNo());
|
||||
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo("SL-" + value.getId()); value.setTemplateId(source.getTemplateId()); value.setChannelId(source.getChannelId()); value.setChannelCodeSnapshot(source.getChannelCodeSnapshot()); value.setChannelNameSnapshot(source.getChannelNameSnapshot()); value.setWarehouseId(source.getWarehouseId()); value.setWarehouseNameSnapshot(source.getWarehouseNameSnapshot()); value.setDeparturePortCode(source.getDeparturePortCode()); value.setDestinationPortCode(source.getDestinationPortCode()); value.setVesselName(source.getVesselName()); value.setVoyageNo(source.getVoyageNo()); value.setEtdAt(request.etdAt()); value.setWarehouseCutoffAt(request.cutoffAt()); value.setEtaAt(copyEta(source, request.etdAt())); value.setStatus("OPEN"); value.setSourceType("COPY"); value.setPlannedCapacityCbm(source.getPlannedCapacityCbm()); value.setVersion(0); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); now(value);
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo(documentNumbers.next("SL")); value.setTemplateId(source.getTemplateId()); value.setChannelId(source.getChannelId()); value.setChannelCodeSnapshot(source.getChannelCodeSnapshot()); value.setChannelNameSnapshot(source.getChannelNameSnapshot()); value.setWarehouseId(source.getWarehouseId()); value.setWarehouseNameSnapshot(source.getWarehouseNameSnapshot()); value.setDeparturePortCode(source.getDeparturePortCode()); value.setDestinationPortCode(source.getDestinationPortCode()); value.setVesselName(source.getVesselName()); value.setVoyageNo(source.getVoyageNo()); value.setEtdAt(request.etdAt()); value.setWarehouseCutoffAt(request.cutoffAt()); value.setEtaAt(copyEta(source, request.etdAt())); value.setStatus("OPEN"); value.setSourceType("COPY"); value.setPlannedCapacityCbm(source.getPlannedCapacityCbm()); value.setVersion(0); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); now(value);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) {
|
||||
SailingEntity duplicate = repository.sailingByBusinessKey(source.getWarehouseId(), source.getChannelCodeSnapshot(),
|
||||
source.getDeparturePortCode(), source.getDestinationPortCode(), request.etdAt(), source.getVoyageNo());
|
||||
@@ -362,7 +364,7 @@ public class SailingCalendarService {
|
||||
if (version(request.expectedVersion()) != 0) throw bad("新建船期expectedVersion必须为0");
|
||||
validateSailing(request.data()); SailingChannelEntity channel = resolveChannel(request.data());
|
||||
if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能创建船期");
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo("SL-" + value.getId());
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo(documentNumbers.next("SL"));
|
||||
copy(value, request.data(), channel); value.setStatus(OPEN); value.setSourceType(normalizeSource(request.data().sourceType()));
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); now(value);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw conflict("船期业务唯一键冲突"); }
|
||||
@@ -398,7 +400,7 @@ public class SailingCalendarService {
|
||||
validateTemplateSnapshot(template);
|
||||
List<SailingEntity> result = new java.util.ArrayList<>();
|
||||
for (GenerationPreview preview : generationPreview(templateId, from, to)) if (!preview.conflict()) {
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo("SL-" + value.getId()); value.setTemplateId(templateId);
|
||||
SailingEntity value = new SailingEntity(); value.setId(IdWorker.getId()); value.setSailingNo(documentNumbers.next("SL")); value.setTemplateId(templateId);
|
||||
value.setChannelId(channel.getId()); value.setChannelCodeSnapshot(channel.getChannelCode()); value.setChannelNameSnapshot(channel.getChannelName());
|
||||
value.setWarehouseId(template.getWarehouseId()); value.setWarehouseNameSnapshot(template.getWarehouseNameSnapshot()); value.setDeparturePortCode(template.getDeparturePortCode()); value.setDestinationPortCode(template.getDestinationPortCode()); value.setVesselName(template.getVesselName()); value.setVoyageNo(preview.voyageNo()); value.setEtdAt(preview.etdAt()); value.setWarehouseCutoffAt(preview.cutoffAt()); value.setStatus(OPEN); value.setSourceType("TEMPLATE"); value.setGeneratedKey(key(template, preview.etdAt(), preview.voyageNo())); value.setVersion(0); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); now(value); repository.insert(value); recordHistory(value.getId(), "GENERATE", null, OPEN, "按固定周模板生成", operatorId, operatorName, value.getCreatedAt()); result.add(value);
|
||||
}
|
||||
|
||||
+23
-47
@@ -1,71 +1,47 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.number.DailyDocumentNumber;
|
||||
import com.baiyue.firstleg.repository.DocumentSequenceRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/** 按北京时间生成两至四位前缀、八位日期和四位日流水。 */
|
||||
@Component
|
||||
public class FirstlegDocumentNumberGenerator {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd", Locale.ROOT);
|
||||
private static final Set<String> SUPPORTED_PREFIXES = Set.of("OB", "TA", "LP", "DP");
|
||||
private static final long MAX_DAILY_SEQUENCE = 9_999L;
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private static final Set<String> PREFIXES = Set.of("OB", "TA", "LP", "DECL", "BR", "PS", "SL", "ST", "CLG");
|
||||
private final DocumentSequenceRepository sequences;
|
||||
private final Clock clock;
|
||||
|
||||
/** 正式运行使用统一业务时区。 */
|
||||
@Autowired
|
||||
public FirstlegDocumentNumberGenerator(JdbcTemplate jdbc) {
|
||||
this(jdbc, Clock.system(BUSINESS_ZONE));
|
||||
public FirstlegDocumentNumberGenerator(DocumentSequenceRepository sequences) {
|
||||
this(sequences, Clock.system(DailyDocumentNumber.BUSINESS_ZONE));
|
||||
}
|
||||
|
||||
FirstlegDocumentNumberGenerator(JdbcTemplate jdbc, Clock clock) {
|
||||
this.jdbc = jdbc;
|
||||
/** 固定时钟用于跨日及前缀独立性验证。 */
|
||||
FirstlegDocumentNumberGenerator(DocumentSequenceRepository sequences, Clock clock) {
|
||||
this.sequences = sequences;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
/** 只为本服务已登记的业务类型分配号码。 */
|
||||
public String next(String prefix) {
|
||||
if (!SUPPORTED_PREFIXES.contains(prefix)) {
|
||||
throw new IllegalArgumentException("unsupported firstleg document prefix: " + prefix);
|
||||
// 实物标签未纳入本次改号,DP 保留原六位日期及横杠。
|
||||
if ("DP".equals(prefix)) {
|
||||
java.time.LocalDate date = java.time.LocalDate.now(clock.withZone(DailyDocumentNumber.BUSINESS_ZONE));
|
||||
long value = sequences.next("DP:" + date.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE));
|
||||
DailyDocumentNumber.format("DP", date, value);
|
||||
return "DP-" + date.format(java.time.format.DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
+ "-" + String.format(java.util.Locale.ROOT, "%04d", value);
|
||||
}
|
||||
LocalDate sequenceDate = LocalDate.now(clock);
|
||||
String sequenceKey = prefix + ":" + DateTimeFormatter.BASIC_ISO_DATE.format(sequenceDate);
|
||||
long sequence = nextSequence(sequenceKey);
|
||||
if (sequence > MAX_DAILY_SEQUENCE) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当日" + prefix + "单据编号已用尽");
|
||||
}
|
||||
return prefix + "-" + DATE_FORMAT.format(sequenceDate) + "-" +
|
||||
String.format(Locale.ROOT, "%04d", sequence);
|
||||
if (!PREFIXES.contains(prefix)) throw new IllegalArgumentException("未登记的业务编号前缀:" + prefix);
|
||||
return DailyDocumentNumber.next(prefix, clock, sequences::next);
|
||||
}
|
||||
|
||||
/** 平台柜次号是长期业务身份,使用不按日期重置的 D 序号。 */
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
/** 柜次号保持现有全局连续规则,不按日期重置,也不限制为四位。 */
|
||||
public String nextCabinetNo() {
|
||||
return "D" + String.format(Locale.ROOT, "%04d", nextSequence("CABINET"));
|
||||
}
|
||||
|
||||
private long nextSequence(String sequenceKey) {
|
||||
jdbc.update(
|
||||
"INSERT INTO fl_document_sequence(sequence_key,current_value) " +
|
||||
"VALUES(?,LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE " +
|
||||
"current_value=LAST_INSERT_ID(current_value+1)",
|
||||
sequenceKey);
|
||||
Long sequence = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
||||
if (sequence == null || sequence <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "业务编号生成失败");
|
||||
}
|
||||
return sequence;
|
||||
return "D" + String.format(java.util.Locale.ROOT, "%04d", sequences.next("CABINET"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +91,13 @@ public class SmartLoadingService {
|
||||
}
|
||||
return repository.candidates(warehouseId);
|
||||
}
|
||||
/** 按柜次渠道查询候选,关联订舱客户快照不限制其他客户的货物。 */
|
||||
public List<Candidate> candidates(long bookingId, long warehouseId) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
if (!Objects.equals(booking.getWarehouseId(), warehouseId)) throw validation("柜次仓库与候选仓库不一致");
|
||||
if (!StringUtils.hasText(booking.getChannelGroupCode())) throw validation("柜次缺少计划渠道,请重新创建模拟柜");
|
||||
List<Candidate> values = candidates(warehouseId).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
.filter(candidate -> sameChannel(booking, candidate)).toList();
|
||||
return enrichColoadCandidates(booking, repository.planByBooking(bookingId), values);
|
||||
}
|
||||
|
||||
@@ -108,10 +109,11 @@ public class SmartLoadingService {
|
||||
return uniqueCandidateUnits(grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList());
|
||||
}
|
||||
|
||||
/** 推荐保留渠道和显式筛选范围,允许同柜包含多个客户。 */
|
||||
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking,
|
||||
Long customerId, String inboundCode) {
|
||||
List<Candidate> values = repository.candidates(null).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate))
|
||||
.filter(candidate -> sameChannel(booking, candidate))
|
||||
.filter(candidate -> customerId == null || Objects.equals(customerId, candidate.accountId()))
|
||||
.filter(candidate -> !StringUtils.hasText(inboundCode)
|
||||
|| inboundCode.trim().equals(candidate.inboundCode()))
|
||||
@@ -513,6 +515,7 @@ public class SmartLoadingService {
|
||||
return view(assigned, requireBooking(assigned.getContainerBookingId()));
|
||||
}
|
||||
|
||||
/** 原子新增明细或整托组成,使用货物自身客户归属而非订舱客户快照。 */
|
||||
private Plan assign(long planId, AssignRequest request, long operatorId, String operatorName,
|
||||
boolean wholeGroupAssignment, Long palletId, String palletNo,
|
||||
Integer actualSkuQuantity, boolean primaryPalletLine, Candidate frozenCandidate,
|
||||
@@ -535,7 +538,6 @@ public class SmartLoadingService {
|
||||
}
|
||||
inboundHoldGuard.requireInboundNotHeld(candidate.accountId(), candidate.inboundId());
|
||||
if (!wholeGroupAssignment) requireStandalone(candidate.accountId(), candidate.inboundId());
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
var task = loadingPickService.mutationSnapshot(planId);
|
||||
if (task.taskId() != null && "LOADING".equals(task.taskStatus())) {
|
||||
splitPreloadService.ensureForRunningLoadingAllocation(candidate.accountId(), candidate.inboundId(),
|
||||
@@ -604,10 +606,7 @@ public class SmartLoadingService {
|
||||
return assignInbound(planId, request, operatorId, operatorName, false, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves declared cartons in a cabinet before order review. Review status is not an allocation
|
||||
* gate; confirmation/PDA remains blocked until the resulting inbound reaches WAIT_SPLIT.
|
||||
*/
|
||||
/** 待审核订单可跨客户占位;权威订单归属和后续审核测量门禁仍必须满足。 */
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PENDING_REVIEW_ASSIGN", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "待审核订单加入装柜清单")
|
||||
@@ -652,7 +651,6 @@ public class SmartLoadingService {
|
||||
for (InboundAllocation allocation : allocations) {
|
||||
if (allocation.packageCount() == 0) continue;
|
||||
Candidate candidate = allocation.candidate();
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true);
|
||||
OrderCargoClient.CargoBinding cargo = declared.stream()
|
||||
.map(DeclaredCargo::cargo)
|
||||
@@ -722,7 +720,7 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<Candidate> details = frozenDetails == null
|
||||
? manualCandidates(booking).stream()
|
||||
? manualCandidates().stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundId(), request.inboundId())).toList()
|
||||
: List.copyOf(frozenDetails);
|
||||
if (details.isEmpty()) throw conflict("入仓码不存在、已分配完或尚未完成入库");
|
||||
@@ -756,6 +754,7 @@ public class SmartLoadingService {
|
||||
return view(assigned, booking);
|
||||
}
|
||||
|
||||
/** 按入仓单自己的客户和申报事实建立占位,不以订舱客户限制拼柜。 */
|
||||
private Plan assignDeclaredInbound(long planId, AssignInboundRequest request,
|
||||
InboundOrderEntity projection, String operation,
|
||||
String requestHash, long operatorId, String operatorName) {
|
||||
@@ -786,7 +785,6 @@ public class SmartLoadingService {
|
||||
for (InboundAllocation allocation : allocations) {
|
||||
if (allocation.packageCount() == 0) continue;
|
||||
Candidate candidate = allocation.candidate();
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true);
|
||||
OrderCargoClient.CargoBinding cargo = declared.stream().map(DeclaredCargo::cargo)
|
||||
.filter(value -> Objects.equals(value.cargoId(), candidate.inboundDetailId()))
|
||||
@@ -1007,6 +1005,7 @@ public class SmartLoadingService {
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_ITEM_ADJUST", resourceType = "LOADING_PLAN_ITEM",
|
||||
resourceId = "#itemId", description = "调整智能装柜明细")
|
||||
/** 调整本计划明细箱数,保留原客户归属、可用量、拣货和容量校验。 */
|
||||
public Plan adjust(long planId, long itemId, AdjustRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ADJUST|" + planId + '|' + itemId + '|'
|
||||
@@ -1053,7 +1052,6 @@ public class SmartLoadingService {
|
||||
if (request.packageCount() > maxPackageCount) {
|
||||
throw validation("调整箱数超过可用箱数");
|
||||
}
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
||||
checkRouteAndCapacity(booking, others, candidate, request.packageCount(), true);
|
||||
@@ -1360,7 +1358,7 @@ public class SmartLoadingService {
|
||||
if (!"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) {
|
||||
throw conflict("同柜不可拆组已锁定或已消费");
|
||||
}
|
||||
List<Candidate> available = manualCandidates(booking);
|
||||
List<Candidate> available = manualCandidates();
|
||||
Map<Long, List<Candidate>> result = new LinkedHashMap<>();
|
||||
for (Member member : group.members()) {
|
||||
if (!repository.activeItemsByInbound(member.tenantId(), member.inboundId()).isEmpty()) {
|
||||
@@ -1382,14 +1380,15 @@ public class SmartLoadingService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 整组推荐可跨客户,但仍按柜次渠道选择候选。 */
|
||||
private List<Candidate> availableCandidates(ContainerBookingEntity booking) {
|
||||
return repository.candidates(null).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
.filter(candidate -> sameChannel(booking, candidate)).toList();
|
||||
}
|
||||
|
||||
private List<Candidate> manualCandidates(ContainerBookingEntity booking) {
|
||||
return repository.candidates(null).stream()
|
||||
.filter(candidate -> sameCustomerScope(booking, candidate)).toList();
|
||||
/** 平台人工分配可拼装多个客户,明细始终保留各自的真实客户归属。 */
|
||||
private List<Candidate> manualCandidates() {
|
||||
return repository.candidates(null);
|
||||
}
|
||||
|
||||
private boolean groupFits(ContainerBookingEntity booking, List<LoadingPlanItemEntity> planItems,
|
||||
@@ -1807,12 +1806,6 @@ public class SmartLoadingService {
|
||||
return StringUtils.hasText(booking.getChannelGroupCode()) && candidateGroup != null
|
||||
&& booking.getChannelGroupCode().equals(candidateGroup.code());
|
||||
}
|
||||
private static boolean sameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
return booking.getCustomerId() == null || Objects.equals(booking.getCustomerId(), candidate.accountId());
|
||||
}
|
||||
private static void requireSameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
if (!sameCustomerScope(booking, candidate)) throw forbidden();
|
||||
}
|
||||
/**
|
||||
* A draft may legitimately contain pre-review or pre-measurement members without a fee snapshot.
|
||||
* The pricing port reports that as NOT_READY, while any sent/customer-confirmed member fails and
|
||||
@@ -1911,5 +1904,5 @@ public class SmartLoadingService {
|
||||
}
|
||||
|
||||
private static String trim(String x){return x==null?null:x.trim();} private static BigDecimal unit(BigDecimal x){return x==null?BigDecimal.ZERO:x.max(BigDecimal.ZERO);}
|
||||
private static void requireAccount(long x){if(x<=0)throw new BizException(ResultCode.BAD_REQUEST,"accountId必须大于零");} private static BizException conflict(String x){return new BizException(ResultCode.CONFLICT,x);} private static BizException validation(String x){return new BizException(ResultCode.VALIDATION_FAILED,x);} private static BizException forbidden(){return new BizException(ResultCode.FORBIDDEN,"客户归属不匹配");}
|
||||
private static void requireAccount(long x){if(x<=0)throw new BizException(ResultCode.BAD_REQUEST,"accountId必须大于零");} private static BizException conflict(String x){return new BizException(ResultCode.CONFLICT,x);} private static BizException validation(String x){return new BizException(ResultCode.VALIDATION_FAILED,x);}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user