This commit is contained in:
2026-08-15 09:47:17 +08:00
parent 1982a7dd69
commit 20907e4685
22 changed files with 236 additions and 327 deletions
@@ -29,7 +29,7 @@ public interface CustomerTransitStatisticsMapper {
JOIN fl_ocean_booking b ON b.id = c.ocean_booking_id
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND b.status <> 'CANCELLED'
AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan p
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id
@@ -43,7 +43,7 @@ public interface CustomerTransitStatisticsMapper {
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND b.status <> 'CANCELLED'
AND i.account_id IS NOT NULL AND i.account_id > 0
) x
WHERE x.customer_id = #{customerId}
@@ -36,23 +36,11 @@ public class PlatformOceanBookingController {
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:create')")
@BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="创建海运订舱单", captureBefore=false)
@BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="登记已确认海运订舱", captureBefore=false)
public ApiResponse<Detail> create(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.create(user.userId(), user.username(), key, request)); }
@PutMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:edit')")
@BusinessAudit(event="OCEAN_BOOKING_UPDATE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="编辑海运订舱草稿")
public ApiResponse<Detail> updateDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpdateDraftRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.updateDraft(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/submit") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:submit')")
@BusinessAudit(event="OCEAN_BOOKING_SUBMIT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="提交海运订舱")
public ApiResponse<Detail> submit(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.submit(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认海运订舱")
public ApiResponse<Detail> confirm(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirm(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/release") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:release')")
@BusinessAudit(event="OCEAN_BOOKING_RELEASE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认放舱")
public ApiResponse<Detail> release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReleaseRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记海运订舱失败")
public ApiResponse<Detail> fail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.fail(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/cancel") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:cancel')")
@BusinessAudit(event="OCEAN_BOOKING_CANCEL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="取消海运订舱")
public ApiResponse<Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.cancel(oceanBookingId, user.userId(), user.username(), key, request)); }
@@ -62,8 +50,5 @@ public class PlatformOceanBookingController {
@PostMapping("/{oceanBookingId}/change-confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认变更船期")
public ApiResponse<Detail> confirmChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirmChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记变更船期失败")
public ApiResponse<Detail> failChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.failChange(oceanBookingId, user.userId(), user.username(), key, request)); }
private static void validateOptionalCustomer(AuthenticatedUser user, Long customerId) { FirstlegScopeGuard.platform(user); if (customerId != null) FirstlegScopeGuard.platform(user, customerId); }
}
@@ -2,10 +2,8 @@ package com.baiyue.firstleg.oceanbooking.domain;
public enum OceanBookingStatus {
DRAFT,
SUBMITTED,
CONFIRMED,
RELEASED,
FAILED,
CHANGE_PENDING,
CANCELLED
}
@@ -44,13 +44,13 @@ public final class OceanBookingDtos {
@NotBlank @Size(max = 64) String channelCode,
@NotNull @Positive Long warehouseId,
@NotBlank @Size(max = 128) String warehouseName,
@Size(max = 128) String billOfLadingNo,
@NotBlank @Size(max = 128) String billOfLadingNo,
@Positive Long shippingCompanyId,
@Size(max = 128) String shippingCompanyName,
@Positive Long bookingAgentId,
@Size(max = 128) String bookingAgentName,
@Size(max = 128) String vesselName,
@Size(max = 64) String voyageNo,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
@Size(max = 32) String departurePortCode,
@Size(max = 128) String departurePortName,
@Size(max = 32) String destinationPortCode,
@@ -58,16 +58,14 @@ public final class OceanBookingDtos {
@Size(max = 128) String placeOfDelivery,
@NotBlank @Size(max = 32) String serviceTerm,
@NotBlank @Size(max = 32) String freightTerm,
LocalDateTime estimatedSailingAt,
LocalDateTime estimatedArrivalAt,
@NotNull LocalDateTime estimatedSailingAt,
@NotNull LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String remark,
@Valid @NotEmpty @Size(max = 3) List<PartyRequest> parties,
@Valid @NotEmpty @Size(max = 20) List<ContainerRequirementRequest> containerRequirements) {}
public record CreateRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record UpdateDraftRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record ActionRequest(@NotNull @Min(0) Integer expectedVersion,
@Size(max = 1000) String reason) {}
public record ReleaseRequest(@NotNull @Min(0) Integer expectedVersion,
@@ -45,7 +45,6 @@ public class OceanBookingEntity {
private LocalDateTime pendingEstimatedSailingAt;
private LocalDateTime pendingEstimatedArrivalAt;
private String changeReason;
private String failureReason;
private String cancellationReason;
private String remark;
private Long createdBy;
@@ -30,31 +30,13 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
Page<OceanBookingEntity> selectPage(Page<OceanBookingEntity> page, @Param("query") Query query);
@Update("""
UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},
channel_code=#{value.channelCode},channel_name=#{value.channelName},warehouse_id=#{value.warehouseId},warehouse_name=#{value.warehouseName},
bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},
shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},
booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},
departure_port_code=#{value.departurePortCode},departure_port_name=#{value.departurePortName},
destination_port_code=#{value.destinationPortCode},destination_port_name=#{value.destinationPortName},
place_of_delivery=#{value.placeOfDelivery},service_term=#{value.serviceTerm},freight_term=#{value.freightTerm},
estimated_sailing_at=#{value.estimatedSailingAt},estimated_arrival_at=#{value.estimatedArrivalAt},
remark=#{value.remark},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{value.id} AND status='DRAFT' AND version=#{expectedVersion}
""")
int updateDraft(@Param("value") OceanBookingEntity value, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status=#{toStatus},failure_reason=#{failureReason},
UPDATE fl_ocean_booking SET status=#{toStatus},
cancellation_reason=#{cancellationReason},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status=#{fromStatus} AND version=#{expectedVersion}
""")
int transition(@Param("oceanBookingId") long oceanBookingId, @Param("fromStatus") String fromStatus,
@Param("toStatus") String toStatus, @Param("failureReason") String failureReason,
@Param("cancellationReason") String cancellationReason,
@Param("toStatus") String toStatus, @Param("cancellationReason") String cancellationReason,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@@ -28,12 +28,9 @@ public class OceanBookingRepository {
}
public OceanBookingEntity find(long id) { return bookings.selectById(id); }
public void insert(OceanBookingEntity value) { bookings.insert(value); }
public int updateDraft(OceanBookingEntity value, int version, long operatorId, String operatorName) {
return bookings.updateDraft(value, version, operatorId, operatorName);
}
public int transition(long id, String from, String to, String failureReason, String cancellationReason,
public int transition(long id, String from, String to, String cancellationReason,
int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, failureReason, cancellationReason, version, operatorId, operatorName);
return bookings.transition(id, from, to, cancellationReason, version, operatorId, operatorName);
}
public int release(long id, ReleaseRequest request, long operatorId, String operatorName) {
return bookings.release(id, request.billOfLadingNo().trim(), request.vesselName().trim(),
@@ -67,6 +67,7 @@ public class OceanBookingService {
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
if (request.expectedVersion() != 0) throw concurrent();
validateDraftStructure(request.data());
validateRange(request.data().estimatedSailingAt(), request.data().estimatedArrivalAt());
String action = "CREATE";
String hash = hash(action, null, request);
Detail replay = claimOrReplay(null, operatorId, action, key, hash);
@@ -75,6 +76,8 @@ public class OceanBookingService {
OceanBookingEntity value = new OceanBookingEntity();
value.setId(IdWorker.getId());
value.setBookingNo("OB" + value.getId());
// Child-row triggers still require DRAFT while the aggregate is assembled.
// The status transition is in the same transaction, so callers only observe CONFIRMED.
value.setStatus(OceanBookingStatus.DRAFT.name());
copy(value, request.data(), channel.channelName());
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
@@ -82,46 +85,16 @@ public class OceanBookingService {
repository.insert(value);
repository.replaceParties(value.getId(), parties(value.getId(), request.data().parties()));
repository.replaceRequirements(value.getId(), requirements(value.getId(), request.data().containerRequirements()));
addHistory(value.getId(), null, OceanBookingStatus.DRAFT.name(), action, null, null, value, operatorId, operatorName);
return complete(value.getId(), operatorId, action, key, detail(value.getId()));
if (repository.transition(value.getId(), OceanBookingStatus.DRAFT.name(),
OceanBookingStatus.CONFIRMED.name(), null, 0, operatorId, operatorName) != 1) {
throw concurrent();
}
OceanBookingEntity confirmed = required(value.getId());
addHistory(value.getId(), OceanBookingStatus.DRAFT.name(), OceanBookingStatus.CONFIRMED.name(), action,
null, value, confirmed, operatorId, operatorName);
return complete(value.getId(), operatorId, action, key, detail(confirmed));
}
@Transactional
public Detail updateDraft(long oceanBookingId, long operatorId, String operatorName, String key,
UpdateDraftRequest request) {
validateDraftStructure(request.data());
String action = "UPDATE_DRAFT";
Detail replay = claimOrReplay(oceanBookingId, operatorId, action, key, hash(action, oceanBookingId, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(oceanBookingId, request.expectedVersion());
requireStatus(before, OceanBookingStatus.DRAFT);
ChannelOption channel = requireActiveChannel(request.data().warehouseId(), request.data().channelCode());
OceanBookingEntity value = new OceanBookingEntity(); value.setId(oceanBookingId); copy(value, request.data(), channel.channelName());
if (repository.updateDraft(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
repository.replaceParties(oceanBookingId, parties(oceanBookingId, request.data().parties()));
repository.replaceRequirements(oceanBookingId, requirements(oceanBookingId, request.data().containerRequirements()));
return complete(oceanBookingId, operatorId, action, key, detail(oceanBookingId));
}
@Transactional public Detail submit(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "SUBMIT", OceanBookingStatus.DRAFT,
OceanBookingStatus.SUBMITTED, request, false);
}
@Transactional public Detail confirm(long id, long operatorId, String name, String key, ActionRequest request) {
String action = "CONFIRM";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
requireActiveChannel(before.getWarehouseId(), before.getChannelCode());
validateConfirmation(before, repository.requirements(id));
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
null, null, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional public Detail release(long id, long operatorId, String name, String key, ReleaseRequest request) {
validateRange(request.estimatedSailingAt(), request.estimatedArrivalAt());
String action = "RELEASE";
@@ -129,6 +102,7 @@ public class OceanBookingService {
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CONFIRMED);
validateRegisteredTransport(before, request);
if (repository.release(id, request, operatorId, name) != 1) throw concurrent();
repository.syncReleasedTransport(id, request, operatorId, name);
repository.allocations(id).forEach(allocation ->
@@ -139,28 +113,6 @@ public class OceanBookingService {
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail fail(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("失败原因不能为空");
String action = "FAIL";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.FAILED.name(), trim(request.reason()), null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, OceanBookingStatus.SUBMITTED.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail failChange(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("改船失败原因不能为空");
return transition(id, operatorId, name, key, "FAIL_CHANGE", OceanBookingStatus.CHANGE_PENDING,
OceanBookingStatus.FAILED, request, true);
}
@Transactional
public Detail cancel(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("取消原因不能为空");
@@ -172,7 +124,7 @@ public class OceanBookingService {
if (from == OceanBookingStatus.RELEASED || from == OceanBookingStatus.CANCELLED) {
throw conflict("订舱已越过不可逆节点或已经取消");
}
if (repository.transition(id, from.name(), OceanBookingStatus.CANCELLED.name(), null,
if (repository.transition(id, from.name(), OceanBookingStatus.CANCELLED.name(),
trim(request.reason()), request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
@@ -211,20 +163,6 @@ public class OceanBookingService {
return complete(id, operatorId, action, key, detail(after));
}
private Detail transition(long id, long operatorId, String name, String key, String action,
OceanBookingStatus from, OceanBookingStatus to, ActionRequest request,
boolean failure) {
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, from);
if (from == OceanBookingStatus.DRAFT && to == OceanBookingStatus.SUBMITTED) validateSubmission(id);
if (repository.transition(id, from.name(), to.name(), failure ? trim(request.reason()) : null, null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), to.name(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
private Detail claimOrReplay(Long id, long operatorId, String action, String key, String hash) {
validateKey(key);
@@ -266,10 +204,10 @@ public class OceanBookingService {
if (!StringUtils.hasText(data.serviceTerm()) || !StringUtils.hasText(data.freightTerm())) {
throw badRequest("Service Term和Freight Term不能为空");
}
if (data.estimatedSailingAt() != null || data.estimatedArrivalAt() != null
|| StringUtils.hasText(data.billOfLadingNo()) || StringUtils.hasText(data.vesselName())
|| StringUtils.hasText(data.voyageNo())) {
throw badRequest("真实运输数据请在确认放舱时录入");
if (!StringUtils.hasText(data.billOfLadingNo()) || !StringUtils.hasText(data.vesselName())
|| !StringUtils.hasText(data.voyageNo()) || data.estimatedSailingAt() == null
|| data.estimatedArrivalAt() == null) {
throw badRequest("提单号、船名、航次、ETD和ETA不能为空");
}
Set<String> types = new HashSet<>();
for (PartyRequest party : data.parties()) {
@@ -278,14 +216,16 @@ public class OceanBookingService {
}
if (!types.equals(PARTY_TYPES)) throw badRequest("订舱方必须各包含一条SHIPPER、CONSIGNEE和NOTIFY_PARTY");
}
private void validateSubmission(long id) {
Set<String> types = repository.parties(id).stream().map(OceanBookingPartyEntity::getPartyType)
.collect(java.util.stream.Collectors.toSet());
if (!types.equals(PARTY_TYPES) || repository.requirements(id).isEmpty()) {
throw conflict("订舱方或箱型需求不完整,不能提交");
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void validateRegisteredTransport(OceanBookingEntity booking, ReleaseRequest request) {
if (!java.util.Objects.equals(trim(booking.getBillOfLadingNo()), trim(request.billOfLadingNo()))
|| !java.util.Objects.equals(trim(booking.getVesselName()), trim(request.vesselName()))
|| !java.util.Objects.equals(trim(booking.getVoyageNo()), trim(request.voyageNo()))
|| !java.util.Objects.equals(booking.getEstimatedSailingAt(), request.estimatedSailingAt())
|| !java.util.Objects.equals(booking.getEstimatedArrivalAt(), request.estimatedArrivalAt())) {
throw conflict("放舱运输字段必须与已登记的提单、船名、航次、ETD和ETA一致");
}
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private ChannelOption requireActiveChannel(Long originWarehouseId, String channelCode) {
if (originWarehouseId == null || originWarehouseId <= 0) throw badRequest("请选择有效起运仓");
String normalized = normalizeChannel(channelCode);
@@ -312,26 +252,15 @@ public class OceanBookingService {
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
v.setChannelCode(d.channelCode().trim()); v.setChannelName(resolvedChannelName);
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(d.warehouseName().trim());
v.setBillOfLadingNo(null);
v.setBillOfLadingNo(d.billOfLadingNo().trim());
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(trim(d.bookingAgentName()));
v.setVesselName(null); v.setVoyageNo(null);
v.setVesselName(d.vesselName().trim()); v.setVoyageNo(d.voyageNo().trim());
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
v.setDestinationPortCode(trim(d.destinationPortCode())); v.setDestinationPortName(trim(d.destinationPortName()));
v.setPlaceOfDelivery(trim(d.placeOfDelivery())); v.setServiceTerm(trim(d.serviceTerm())); v.setFreightTerm(trim(d.freightTerm()));
v.setEstimatedSailingAt(null); v.setEstimatedArrivalAt(null); v.setRemark(trim(d.remark()));
}
private static void validateConfirmation(OceanBookingEntity booking,
List<OceanBookingContainerRequirementEntity> requirements) {
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(booking.getChannelName())
|| booking.getWarehouseId() == null || !StringUtils.hasText(booking.getWarehouseName())) {
throw conflict("订舱缺少渠道或国内中转仓快照,不能确认");
}
if (requirements.isEmpty() || requirements.stream().anyMatch(requirement ->
requirement.getCapacityCbm() == null || requirement.getMaxWeightKg() == null)) {
throw conflict("箱型需求缺少容量或最大承重快照,不能确认");
}
v.setEstimatedSailingAt(d.estimatedSailingAt()); v.setEstimatedArrivalAt(d.estimatedArrivalAt());
v.setRemark(trim(d.remark()));
}
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }