优化
This commit is contained in:
@@ -31,6 +31,15 @@ public interface OrderCargoClient {
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long orderId);
|
||||
|
||||
@GetMapping("/{tenantId}/{orderId}/measurement-declaration")
|
||||
ApiResponse<MeasurementDeclarationSnapshot> measurementDeclaration(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long orderId);
|
||||
|
||||
record MeasurementDeclarationSnapshot(
|
||||
String referenceId, BigDecimal originalGrossWeightKg,
|
||||
BigDecimal originalVolumeCbm, String defaultActualGoodsName) {}
|
||||
|
||||
record CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
|
||||
BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ public class PlatformInboundMeasurementController {
|
||||
resourceId = "#resultId", description = "确认 OCR 重量并累加实际总毛重")
|
||||
@Operation(summary = "确认 OCR 重量",
|
||||
description = "可在同一事务中携带人工毛重修改值并确认;统一换算 KG、扣除托盘皮重后,"
|
||||
+ "按所有已确认净重重算入库单 actualGrossWeightKg。")
|
||||
+ "按所有已确认结果分别重算入库单毛重与扣皮后的货物净重。")
|
||||
@PostMapping("/weight-results/{resultId}/confirm")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:confirm')"
|
||||
+ " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))")
|
||||
|
||||
-36
@@ -6,7 +6,6 @@ import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementApprovalResponse;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
@@ -57,41 +56,6 @@ public class PlatformInboundMeasurementReviewController {
|
||||
request.measurementId(), request.expectedVersion() + 1));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_REJECT", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量审核驳回")
|
||||
@Operation(summary = "测量审核驳回",
|
||||
description = "驳回原因必填,只退回测量数据修改;已确认的托盘和库位占用保持不动。")
|
||||
@PostMapping("/{inboundId}/measurements/reject")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review')")
|
||||
public ApiResponse<Void> reject(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
service.reject(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_EXCEPTION_RELEASE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量驳回异常放行")
|
||||
@Operation(summary = "测量驳回异常放行",
|
||||
description = "Nacos 开关开启、有异常放行权限且原因非空时,才可从 MEASURE_REJECTED 推进至 WAIT_SPLIT。")
|
||||
@PostMapping("/{inboundId}/measurements/exception-release")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:exception-release')")
|
||||
public ApiResponse<Void> exceptionRelease(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody ExceptionReleaseRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
service.exceptionRelease(
|
||||
tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询测量审核历史")
|
||||
@GetMapping("/{inboundId}/measurement-review-history")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review-history')")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 当前业务单元所关联柜次的正式运输事实。 */
|
||||
public record CurrentContainerTransport(
|
||||
Long inboundId, Long accountId, Long containerBookingId,
|
||||
String containerNo, String cabinetNo, String vesselName, String voyageNo,
|
||||
LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt,
|
||||
LocalDateTime estimatedPortArrivalAt, LocalDateTime actualPortArrivalAt
|
||||
) {}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
@@ -26,12 +27,13 @@ public final class InboundMeasurementRequests {
|
||||
public record SaveDraftRequest(
|
||||
Long measurementId,
|
||||
@NotNull @PositiveOrZero Integer version,
|
||||
@Size(max = 64) String originalDocumentNo,
|
||||
@DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal originalGrossWeightKg,
|
||||
@DecimalMin("0.0000") @Digits(integer = 14, fraction = 4) BigDecimal originalVolumeCbm,
|
||||
@Size(max = 255) String actualGoodsName,
|
||||
@NotEmpty @Size(max = 500) List<@Valid ItemRequest> items
|
||||
) {
|
||||
@JsonAnySetter
|
||||
public void rejectUnknownField(String fieldName, Object ignored) {
|
||||
throw new IllegalArgumentException("测量草稿不接受字段: " + fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
public record ItemRequest(
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class InboundMeasurementResponses {
|
||||
Integer originalPackageCount,
|
||||
Integer actualPackageCount,
|
||||
BigDecimal actualGrossWeightKg,
|
||||
BigDecimal actualNetWeightKg,
|
||||
BigDecimal actualVolumeCbm,
|
||||
BigDecimal volumeWeightKg,
|
||||
String actualGoodsName,
|
||||
|
||||
@@ -28,12 +28,25 @@ public record InboundOrderSummary(
|
||||
LocalDateTime measureCompletedAt, LocalDateTime inboundedAt,
|
||||
String expressCompanyCode, String expressCompanyName, String expressTrackingNo,
|
||||
String containerNo, String cabinetNo,
|
||||
String vesselName, String voyageNo, LocalDateTime sailingAt, LocalDateTime portArrivalAt,
|
||||
String vesselName, String voyageNo,
|
||||
LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt,
|
||||
LocalDateTime estimatedPortArrivalAt, LocalDateTime actualPortArrivalAt,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt
|
||||
) {
|
||||
/** 非统一入仓接口保留既有快照适配;统一订单不得调用此重载。 */
|
||||
public static InboundOrderSummary from(
|
||||
InboundOrderEntity order, InboundTransportSnapshotEntity transport,
|
||||
InboundPickupSnapshotEntity pickup, String locationCodes) {
|
||||
CurrentContainerTransport adapted = transport == null ? null : new CurrentContainerTransport(
|
||||
order.getId(), order.getTenantId(), null, transport.getContainerNo(), transport.getCabinetNo(),
|
||||
transport.getVesselName(), transport.getVoyageNo(), transport.getSailingAt(), null,
|
||||
transport.getPortArrivalAt(), null);
|
||||
return from(order, adapted, pickup, locationCodes);
|
||||
}
|
||||
|
||||
public static InboundOrderSummary from(
|
||||
InboundOrderEntity order, CurrentContainerTransport transport,
|
||||
InboundPickupSnapshotEntity pickup, String locationCodes) {
|
||||
return new InboundOrderSummary(
|
||||
order.getId(), order.getTenantId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
|
||||
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
|
||||
@@ -53,12 +66,14 @@ public record InboundOrderSummary(
|
||||
locationCodes,
|
||||
order.getMeasureCompletedAt(), order.getInboundedAt(), order.getExpressCompanyCode(),
|
||||
order.getExpressCompanyName(), order.getExpressTrackingNo(),
|
||||
transport == null ? null : transport.getContainerNo(),
|
||||
transport == null ? null : transport.getCabinetNo(),
|
||||
transport == null ? null : transport.getVesselName(),
|
||||
transport == null ? null : transport.getVoyageNo(),
|
||||
transport == null ? null : transport.getSailingAt(),
|
||||
transport == null ? null : transport.getPortArrivalAt(),
|
||||
transport == null ? null : transport.containerNo(),
|
||||
transport == null ? null : transport.cabinetNo(),
|
||||
transport == null ? null : transport.vesselName(),
|
||||
transport == null ? null : transport.voyageNo(),
|
||||
transport == null ? null : transport.estimatedSailingAt(),
|
||||
transport == null ? null : transport.actualSailingAt(),
|
||||
transport == null ? null : transport.estimatedPortArrivalAt(),
|
||||
transport == null ? null : transport.actualPortArrivalAt(),
|
||||
order.getCreatedAt(), order.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
|
||||
public record MeasurementSummary(
|
||||
Long measurementId, String sourceType, String measurementStatus,
|
||||
Integer actualPackageCount, BigDecimal actualGrossWeightKg,
|
||||
Integer actualPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg,
|
||||
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName) {
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ public class InboundPricingEntity extends BaseEntity {
|
||||
private Long destinationWarehouseId;
|
||||
private String destinationName;
|
||||
private BigDecimal actualGrossWeightKg;
|
||||
private BigDecimal actualNetWeightKg;
|
||||
private BigDecimal actualVolumeCbm;
|
||||
private BigDecimal chargeableWeightKg;
|
||||
private BigDecimal chargeableVolumeCbm;
|
||||
|
||||
@@ -12,19 +12,14 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
||||
actual_gross_weight_kg = CASE
|
||||
WHEN EXISTS (SELECT 1
|
||||
FROM fl_inbound_weight_result weight
|
||||
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
|
||||
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED')
|
||||
THEN (SELECT COALESCE(SUM(weight.net_weight_kg), 0)
|
||||
FROM fl_inbound_weight_result weight
|
||||
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
|
||||
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED')
|
||||
ELSE (SELECT COALESCE(SUM(item.total_weight_kg), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId})
|
||||
END,
|
||||
actual_gross_weight_kg = (SELECT SUM(weight.gross_weight_kg)
|
||||
FROM fl_inbound_weight_result weight
|
||||
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
|
||||
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED'),
|
||||
actual_net_weight_kg = (SELECT SUM(weight.net_weight_kg)
|
||||
FROM fl_inbound_weight_result weight
|
||||
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
|
||||
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED'),
|
||||
actual_volume_cbm = (SELECT COALESCE(SUM(item.total_volume_cbm), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baiyue.firstleg.dto.CurrentContainerTransport;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 统一订单按当前有效装柜明细批量读取柜次船期。 */
|
||||
@Mapper
|
||||
public interface UnifiedInboundTransportMapper {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT i.inbound_id, i.account_id, p.container_booking_id,
|
||||
b.container_no, b.cabinet_no, b.vessel_name, b.voyage_no,
|
||||
b.estimated_sailing_at, b.actual_sailing_at,
|
||||
b.estimated_port_arrival_at, b.actual_port_arrival_at
|
||||
FROM fl_loading_plan_item i
|
||||
JOIN fl_loading_plan p ON p.id = i.loading_plan_id
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
WHERE i.deleted = 0
|
||||
AND p.plan_status IN ('LOADING', 'COMPLETED')
|
||||
AND i.inbound_id IN
|
||||
<foreach collection="inboundIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</script>
|
||||
""")
|
||||
List<CurrentContainerTransport> findCurrentByInboundIds(@Param("inboundIds") List<Long> inboundIds);
|
||||
}
|
||||
+2
-1
@@ -59,7 +59,8 @@ public class InboundMeasurementRepository {
|
||||
return inboundOrders.update(inbound, new LambdaUpdateWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, inbound.getTenantId())
|
||||
.eq(InboundOrderEntity::getId, inbound.getId())
|
||||
.set(InboundOrderEntity::getActualGrossWeightKg, inbound.getActualGrossWeightKg())) == 1;
|
||||
.set(InboundOrderEntity::getActualGrossWeightKg, inbound.getActualGrossWeightKg())
|
||||
.set(InboundOrderEntity::getActualNetWeightKg, inbound.getActualNetWeightKg())) == 1;
|
||||
}
|
||||
|
||||
public InboundMeasurementEntity findMeasurement(long tenantId, long inboundId, long measurementId) {
|
||||
|
||||
@@ -154,29 +154,31 @@ public class InboundOrderRepository {
|
||||
+ "OR (newer.fee_version = f.fee_version AND newer.id > f.id))))",
|
||||
query.feeReviewStatus())
|
||||
.apply(StringUtils.hasText(query.containerNo()),
|
||||
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
|
||||
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
|
||||
+ "AND t.container_no LIKE CONCAT('%', {0}, '%'))", query.containerNo())
|
||||
"EXISTS (SELECT 1 FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id "
|
||||
+ "JOIN fl_container_booking b ON b.id=p.container_booking_id WHERE i.deleted=0 "
|
||||
+ "AND p.plan_status IN ('LOADING','COMPLETED') AND i.account_id=fl_inbound_order.tenant_id "
|
||||
+ "AND i.inbound_id=fl_inbound_order.id AND b.container_no LIKE CONCAT('%', {0}, '%'))", query.containerNo())
|
||||
.apply(StringUtils.hasText(query.cabinetNo()),
|
||||
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
|
||||
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
|
||||
+ "AND t.cabinet_no LIKE CONCAT('%', {0}, '%'))", query.cabinetNo())
|
||||
"EXISTS (SELECT 1 FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id "
|
||||
+ "JOIN fl_container_booking b ON b.id=p.container_booking_id WHERE i.deleted=0 "
|
||||
+ "AND p.plan_status IN ('LOADING','COMPLETED') AND i.account_id=fl_inbound_order.tenant_id "
|
||||
+ "AND i.inbound_id=fl_inbound_order.id AND b.cabinet_no LIKE CONCAT('%', {0}, '%'))", query.cabinetNo())
|
||||
.apply(query.sailingDate() != null,
|
||||
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
|
||||
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
|
||||
+ "AND t.sailing_at >= {0} AND t.sailing_at < {1})",
|
||||
"EXISTS (SELECT 1 FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id "
|
||||
+ "JOIN fl_container_booking b ON b.id=p.container_booking_id WHERE i.deleted=0 "
|
||||
+ "AND p.plan_status IN ('LOADING','COMPLETED') AND i.account_id=fl_inbound_order.tenant_id "
|
||||
+ "AND i.inbound_id=fl_inbound_order.id AND COALESCE(b.actual_sailing_at,b.estimated_sailing_at) "
|
||||
+ ">= {0} AND COALESCE(b.actual_sailing_at,b.estimated_sailing_at) < {1})",
|
||||
query.sailingDate() == null ? null : query.sailingDate().atStartOfDay(),
|
||||
query.sailingDate() == null ? null : query.sailingDate().plusDays(1).atStartOfDay())
|
||||
.apply(Boolean.TRUE.equals(query.loaded()),
|
||||
"EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
|
||||
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
|
||||
+ "AND COALESCE(TRIM(t.loading_status), '') <> '' "
|
||||
+ "AND t.loading_status NOT IN ('UNLOADED', 'PENDING'))")
|
||||
"EXISTS (SELECT 1 FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id "
|
||||
+ "WHERE i.deleted=0 AND p.plan_status='COMPLETED' "
|
||||
+ "AND i.account_id=fl_inbound_order.tenant_id AND i.inbound_id=fl_inbound_order.id)")
|
||||
.apply(Boolean.FALSE.equals(query.loaded()),
|
||||
"NOT EXISTS (SELECT 1 FROM fl_inbound_transport_snapshot t "
|
||||
+ "WHERE t.tenant_id = fl_inbound_order.tenant_id AND t.inbound_id = fl_inbound_order.id "
|
||||
+ "AND COALESCE(TRIM(t.loading_status), '') <> '' "
|
||||
+ "AND t.loading_status NOT IN ('UNLOADED', 'PENDING'))")
|
||||
"NOT EXISTS (SELECT 1 FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id "
|
||||
+ "WHERE i.deleted=0 AND p.plan_status='COMPLETED' "
|
||||
+ "AND i.account_id=fl_inbound_order.tenant_id AND i.inbound_id=fl_inbound_order.id)")
|
||||
.ge(query.createdFrom() != null, InboundOrderEntity::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, InboundOrderEntity::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(InboundOrderEntity::getCreatedAt).orderByDesc(InboundOrderEntity::getId);
|
||||
|
||||
+19
-13
@@ -8,9 +8,11 @@ import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import com.baiyue.firstleg.split.mapper.PreloadPlanMapper;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
|
||||
import com.baiyue.firstleg.dto.CurrentContainerTransport;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundTransportSnapshotMapper;
|
||||
import com.baiyue.firstleg.mapper.UnifiedInboundTransportMapper;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -26,7 +28,7 @@ public class UnifiedInboundReadRepository {
|
||||
@Resource private InboundLoadingPickTaskMapper picks;
|
||||
@Resource private LoadingEvidenceMapper evidences;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private InboundTransportSnapshotMapper transports;
|
||||
@Resource private UnifiedInboundTransportMapper transports;
|
||||
|
||||
public PreloadPlanEntity findPreload(long tenantId, long inboundId) {
|
||||
return preloads.selectOne(new LambdaQueryWrapper<PreloadPlanEntity>()
|
||||
@@ -84,19 +86,23 @@ public class UnifiedInboundReadRepository {
|
||||
Function.identity(), (latest, ignored) -> latest));
|
||||
}
|
||||
|
||||
public InboundTransportSnapshotEntity findTransport(long tenantId, long inboundId) {
|
||||
return transports.selectOne(new LambdaQueryWrapper<InboundTransportSnapshotEntity>()
|
||||
.eq(InboundTransportSnapshotEntity::getTenantId, tenantId)
|
||||
.eq(InboundTransportSnapshotEntity::getInboundId, inboundId));
|
||||
public CurrentContainerTransport findTransport(long tenantId, long inboundId) {
|
||||
return findTransports(Map.of(inboundId, tenantId)).get(inboundId);
|
||||
}
|
||||
|
||||
public Map<Long, InboundTransportSnapshotEntity> findTransports(Map<Long, Long> tenantByInbound) {
|
||||
public Map<Long, CurrentContainerTransport> findTransports(Map<Long, Long> tenantByInbound) {
|
||||
if (tenantByInbound.isEmpty()) return Map.of();
|
||||
return transports.selectList(new LambdaQueryWrapper<InboundTransportSnapshotEntity>()
|
||||
.in(InboundTransportSnapshotEntity::getInboundId, tenantByInbound.keySet()))
|
||||
.stream().filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||
.collect(Collectors.toMap(InboundTransportSnapshotEntity::getInboundId,
|
||||
Function.identity(), (left, right) -> left));
|
||||
Map<Long, List<CurrentContainerTransport>> grouped = transports
|
||||
.findCurrentByInboundIds(List.copyOf(tenantByInbound.keySet())).stream()
|
||||
.filter(item -> owns(tenantByInbound, item.inboundId(), item.accountId()))
|
||||
.collect(Collectors.groupingBy(CurrentContainerTransport::inboundId));
|
||||
grouped.forEach((inboundId, rows) -> {
|
||||
long bookingCount = rows.stream().map(CurrentContainerTransport::containerBookingId).distinct().count();
|
||||
if (bookingCount > 1) throw new BizException(ResultCode.CONFLICT,
|
||||
"同一入库业务单元关联多个有效柜次: inboundId=" + inboundId);
|
||||
});
|
||||
return grouped.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> entry.getValue().get(0)));
|
||||
}
|
||||
|
||||
private static boolean owns(Map<Long, Long> tenantByInbound, Long inboundId, Long tenantId) {
|
||||
|
||||
@@ -150,21 +150,24 @@ public class CartonMeasurementService {
|
||||
.filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
|
||||
.toList();
|
||||
if (!confirmedWeights.isEmpty()) {
|
||||
if (confirmedWeights.stream().anyMatch(weight -> !positive(weight.getGrossWeightKg())
|
||||
|| !positive(weight.getNetWeightKg()))) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"已确认称重结果缺少有效毛重或净重,请重新确认后再修改箱数");
|
||||
}
|
||||
BigDecimal confirmedGrossWeight = confirmedWeights.stream()
|
||||
.map(InboundWeightResultEntity::getGrossWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal confirmedNetWeight = confirmedWeights.stream()
|
||||
.map(InboundWeightResultEntity::getNetWeightKg)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.CEILING);
|
||||
inbound.setActualGrossWeightKg(confirmedNetWeight);
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
inbound.setActualGrossWeightKg(confirmedGrossWeight);
|
||||
inbound.setActualNetWeightKg(confirmedNetWeight);
|
||||
} else {
|
||||
BigDecimal totalWeight = items.stream()
|
||||
.map(InboundMeasurementItemEntity::getTotalWeightKg)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.CEILING);
|
||||
if (positive(totalWeight)) {
|
||||
inbound.setActualGrossWeightKg(totalWeight);
|
||||
}
|
||||
inbound.setActualGrossWeightKg(null);
|
||||
inbound.setActualNetWeightKg(null);
|
||||
}
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
|
||||
@@ -58,9 +58,9 @@ public class FbaPricingService {
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
pricingTaxMode(inbound), inbound.getActualGrossWeightKg(), inbound.getActualVolumeCbm()));
|
||||
pricingTaxMode(inbound), inbound.getActualNetWeightKg(), inbound.getActualVolumeCbm()));
|
||||
RateRow rate = estimate.rate();
|
||||
BigDecimal weight = inbound.getActualGrossWeightKg().setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal weight = inbound.getActualNetWeightKg().setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal volume = inbound.getActualVolumeCbm().setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal chargeable = estimate.chargeableQuantity();
|
||||
BigDecimal converted = chargeable;
|
||||
@@ -240,9 +240,9 @@ public class FbaPricingService {
|
||||
throw conflict("当前目的地类型不支持自动计费");
|
||||
}
|
||||
if (!"APPROVED".equals(inbound.getMeasureReviewStatus())) throw conflict("只能消费审核通过的测量快照");
|
||||
if (inbound.getActualGrossWeightKg() == null || inbound.getActualGrossWeightKg().signum() <= 0
|
||||
if (inbound.getActualNetWeightKg() == null || inbound.getActualNetWeightKg().signum() <= 0
|
||||
|| inbound.getActualVolumeCbm() == null || inbound.getActualVolumeCbm().signum() <= 0) {
|
||||
throw conflict("审核通过的测量快照必须包含大于零的实重和体积");
|
||||
throw conflict("审核通过的测量快照必须包含大于零的货物净重和体积");
|
||||
}
|
||||
if (inbound.getCustomerId() == null || inbound.getCustomerId() <= 0) {
|
||||
throw conflict("入库单缺少订单客户归属,不能生成对客费用");
|
||||
|
||||
@@ -390,7 +390,8 @@ public class InboundFeeDetailService {
|
||||
value.setDestinationCode(inbound.getDestinationCode());
|
||||
value.setDestinationName(inbound.getDestinationName());
|
||||
value.setPackageCount(inbound.getTotalPackageCount());
|
||||
value.setActualGrossWeightKg(inbound.getActualGrossWeightKg());
|
||||
// 财务历史字段名保留,但计费事实统一使用扣除托盘皮重后的货物净重。
|
||||
value.setActualGrossWeightKg(inbound.getActualNetWeightKg());
|
||||
value.setActualVolumeCbm(inbound.getActualVolumeCbm());
|
||||
value.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
||||
value.setChargeableWeightKg(inbound.getChargeableWeightKg());
|
||||
|
||||
+118
-41
@@ -68,7 +68,9 @@ public class InboundMeasurementService {
|
||||
? List.of() : repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inboundId);
|
||||
return toDetail(inbound, measurement, items, weights, review);
|
||||
OrderCargoClient.MeasurementDeclarationSnapshot declaration = needsDeclarationFallback(measurement)
|
||||
? loadDeclaration(inbound) : null;
|
||||
return toDetail(inbound, measurement, items, weights, review, declaration);
|
||||
}
|
||||
|
||||
public List<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||
@@ -87,11 +89,23 @@ public class InboundMeasurementService {
|
||||
InboundMeasurementRequests.SaveDraftRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
validateInboundDetails(tenantId, inbound, request.items());
|
||||
InboundMeasurementEntity existing = request.measurementId() == null ? null
|
||||
: repository.findMeasurement(tenantId, inboundId, request.measurementId());
|
||||
OrderCargoClient.MeasurementDeclarationSnapshot declaration = loadDeclaration(inbound);
|
||||
String actualGoodsName = StringUtils.hasText(request.actualGoodsName()) ? request.actualGoodsName().trim()
|
||||
: existing != null && StringUtils.hasText(existing.getActualGoodsName())
|
||||
? existing.getActualGoodsName() : defaultGoodsName(declaration);
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
tenantId, inboundId, request.measurementId(), request.version(),
|
||||
MeasurementBatchService.SOURCE_MANUAL, "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, request.originalDocumentNo(),
|
||||
request.originalGrossWeightKg(), request.originalVolumeCbm(), request.actualGoodsName(),
|
||||
LocalDateTime.now(), null,
|
||||
existing != null && StringUtils.hasText(existing.getOriginalDocumentNo())
|
||||
? existing.getOriginalDocumentNo() : declaration.referenceId(),
|
||||
existing != null && existing.getOriginalGrossWeightKg() != null
|
||||
? existing.getOriginalGrossWeightKg() : declaration.originalGrossWeightKg(),
|
||||
existing != null && existing.getOriginalVolumeCbm() != null
|
||||
? existing.getOriginalVolumeCbm() : declaration.originalVolumeCbm(),
|
||||
actualGoodsName,
|
||||
operatorId, request.items().stream().map(item -> new MeasurementBatchCommand.Item(
|
||||
item.inboundDetailId(), item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(),
|
||||
item.widthCm(), item.heightCm(), item.unitWeightKg(), item.intelligentImageFileId(),
|
||||
@@ -116,10 +130,7 @@ public class InboundMeasurementService {
|
||||
}
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
BigDecimal confirmedWeight = confirmedWeightForSubmission(weights);
|
||||
int confirmedPalletCount = Math.toIntExact(weights.stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.count());
|
||||
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
|
||||
|
||||
if (!alreadyAdvanced) {
|
||||
requireConfirmedPackageTotal(inbound, items);
|
||||
@@ -137,10 +148,9 @@ public class InboundMeasurementService {
|
||||
if (measurement.getTotalPackageCount() != null) {
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
}
|
||||
if (confirmedWeight != null) {
|
||||
inbound.setActualGrossWeightKg(confirmedWeight);
|
||||
}
|
||||
inbound.setPalletCount(confirmedPalletCount);
|
||||
inbound.setActualGrossWeightKg(confirmedTotals.grossWeightKg());
|
||||
inbound.setActualNetWeightKg(confirmedTotals.netWeightKg());
|
||||
inbound.setPalletCount(confirmedTotals.palletCount());
|
||||
if (measurement.getTotalVolumeCbm() != null) {
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
}
|
||||
@@ -161,6 +171,7 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.CreateWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireWeightEditableInbound(inbound);
|
||||
if (repository.findWeightByFile(tenantId, inboundId, request.fileId()) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "同一称重图片不能重复计重");
|
||||
}
|
||||
@@ -192,6 +203,7 @@ public class InboundMeasurementService {
|
||||
weight.setDeleted(false);
|
||||
weight.setVersion(0);
|
||||
repository.insertWeight(weight);
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
return toWeight(weight);
|
||||
}
|
||||
|
||||
@@ -200,14 +212,12 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long resultId, long operatorId, String operatorName,
|
||||
InboundMeasurementRequests.CorrectWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireWeightEditableInbound(inbound);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
boolean wasConfirmed = WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus());
|
||||
applyWeightCorrection(weight, request.version(), request.adjustedWeightValue(),
|
||||
request.adjustedWeightUnit(), request.adjustmentReason(), operatorId, operatorName);
|
||||
if (wasConfirmed) {
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
}
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
return toWeight(weight);
|
||||
}
|
||||
|
||||
@@ -246,6 +256,7 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long resultId, long operatorId, String operatorName,
|
||||
InboundMeasurementRequests.ConfirmWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireWeightConfirmableInbound(inbound, request.hasAdjustment());
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
int currentVersion = request.version();
|
||||
@@ -291,19 +302,15 @@ public class InboundMeasurementService {
|
||||
|
||||
@Transactional
|
||||
public void deleteWeightResult(long tenantId, long inboundId, long resultId,
|
||||
int version, long operatorId) {
|
||||
int version, long operatorId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
if (!WEIGHT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前入库状态不允许删除称重图片");
|
||||
}
|
||||
requireWeightEditableInbound(inbound);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(version, weight.getVersion());
|
||||
if (!repository.deleteWeight(tenantId, inboundId, resultId, version)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
}
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
try {
|
||||
ApiResponse<Void> response = imageClient.delete(
|
||||
internalToken, operatorId, tenantId, inboundId, weight.getFileId());
|
||||
@@ -319,15 +326,10 @@ public class InboundMeasurementService {
|
||||
}
|
||||
|
||||
private void recalculateConfirmedWeight(InboundOrderEntity inbound, long operatorId) {
|
||||
List<InboundWeightResultEntity> confirmedWeights = repository
|
||||
.listWeights(inbound.getTenantId(), inbound.getId()).stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.toList();
|
||||
BigDecimal total = confirmedWeights.stream()
|
||||
.map(InboundWeightResultEntity::getNetWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
inbound.setActualGrossWeightKg(confirmedWeights.isEmpty() ? null : total);
|
||||
ConfirmedWeightTotals totals = confirmedWeightTotals(
|
||||
repository.listWeights(inbound.getTenantId(), inbound.getId()));
|
||||
inbound.setActualGrossWeightKg(totals.grossWeightKg());
|
||||
inbound.setActualNetWeightKg(totals.netWeightKg());
|
||||
if ("WAIT_MEASURE".equals(inbound.getProcessStatus())) {
|
||||
inbound.setProcessStatus("MEASURING");
|
||||
}
|
||||
@@ -337,13 +339,31 @@ public class InboundMeasurementService {
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal confirmedWeightForSubmission(List<InboundWeightResultEntity> weights) {
|
||||
BigDecimal total = weights.stream()
|
||||
private ConfirmedWeightTotals confirmedWeightTotals(List<InboundWeightResultEntity> weights) {
|
||||
List<InboundWeightResultEntity> confirmed = weights.stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.toList();
|
||||
if (confirmed.isEmpty()) {
|
||||
return new ConfirmedWeightTotals(null, null, 0);
|
||||
}
|
||||
if (confirmed.stream().anyMatch(weight -> !isPositive(weight.getGrossWeightKg())
|
||||
|| !isPositive(weight.getNetWeightKg()))) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"已确认称重结果缺少有效毛重或净重,请重新确认后再继续");
|
||||
}
|
||||
BigDecimal grossWeightKg = confirmed.stream()
|
||||
.map(InboundWeightResultEntity::getGrossWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal netWeightKg = confirmed.stream()
|
||||
.map(InboundWeightResultEntity::getNetWeightKg)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(3, RoundingMode.HALF_UP);
|
||||
return isPositive(total) ? total : null;
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
return new ConfirmedWeightTotals(grossWeightKg, netWeightKg, confirmed.size());
|
||||
}
|
||||
|
||||
private record ConfirmedWeightTotals(
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, int palletCount) {
|
||||
}
|
||||
|
||||
private void requireCompleteAdjustment(BigDecimal value, String unit, String reason) {
|
||||
@@ -353,6 +373,23 @@ public class InboundMeasurementService {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireWeightEditableInbound(InboundOrderEntity inbound) {
|
||||
if (!WEIGHT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前入库状态不允许修改称重结果");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireWeightConfirmableInbound(InboundOrderEntity inbound, boolean hasAdjustment) {
|
||||
if (WEIGHT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
return;
|
||||
}
|
||||
if ("MEASURE_PENDING".equals(inbound.getProcessStatus()) && hasAdjustment) {
|
||||
return;
|
||||
}
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"当前入库状态不允许确认称重结果,待审核状态仅允许人工修正并确认");
|
||||
}
|
||||
|
||||
private void insertRevision(InboundWeightResultEntity weight, int revisionNo, String action,
|
||||
BigDecimal beforeValue, String beforeUnit,
|
||||
BigDecimal afterValue, String afterUnit,
|
||||
@@ -380,7 +417,7 @@ public class InboundMeasurementService {
|
||||
private InboundMeasurementResponses.Detail toDetail(
|
||||
InboundOrderEntity inbound, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> items, List<InboundWeightResultEntity> weights,
|
||||
MeasureReviewRecordEntity review) {
|
||||
MeasureReviewRecordEntity review, OrderCargoClient.MeasurementDeclarationSnapshot declaration) {
|
||||
return new InboundMeasurementResponses.Detail(
|
||||
inbound.getId(), inbound.getInboundCode(), inbound.getCompanyName(), inbound.getChannelCode(),
|
||||
inbound.getChannelName(), inbound.getProcessStatus(), inbound.getMeasureReviewStatus(),
|
||||
@@ -391,18 +428,58 @@ public class InboundMeasurementService {
|
||||
measurement == null ? null : measurement.getStatus(),
|
||||
inbound.getVersion(),
|
||||
measurement == null ? null : measurement.getVersion(),
|
||||
measurement == null ? null : measurement.getOriginalDocumentNo(),
|
||||
measurement == null ? null : measurement.getOriginalGrossWeightKg(),
|
||||
measurement == null ? null : measurement.getOriginalVolumeCbm(),
|
||||
measurement != null && StringUtils.hasText(measurement.getOriginalDocumentNo())
|
||||
? measurement.getOriginalDocumentNo() : declaration == null ? null : declaration.referenceId(),
|
||||
measurement != null && measurement.getOriginalGrossWeightKg() != null
|
||||
? measurement.getOriginalGrossWeightKg() : declaration == null ? null : declaration.originalGrossWeightKg(),
|
||||
measurement != null && measurement.getOriginalVolumeCbm() != null
|
||||
? measurement.getOriginalVolumeCbm() : declaration == null ? null : declaration.originalVolumeCbm(),
|
||||
inbound.getTotalPackageCount(),
|
||||
measurement == null ? inbound.getTotalPackageCount() : measurement.getTotalPackageCount(),
|
||||
inbound.getActualGrossWeightKg(),
|
||||
inbound.getActualNetWeightKg(),
|
||||
measurement == null ? inbound.getActualVolumeCbm() : measurement.getTotalVolumeCbm(),
|
||||
measurement == null ? inbound.getVolumeWeightKg() : measurement.getVolumeWeightKg(),
|
||||
measurement == null ? null : measurement.getActualGoodsName(),
|
||||
measurement != null && StringUtils.hasText(measurement.getActualGoodsName())
|
||||
? measurement.getActualGoodsName() : declaration == null ? null : declaration.defaultActualGoodsName(),
|
||||
items.stream().map(this::toItem).toList(), weights.stream().map(this::toWeight).toList());
|
||||
}
|
||||
|
||||
private boolean needsDeclarationFallback(InboundMeasurementEntity measurement) {
|
||||
return measurement == null || !StringUtils.hasText(measurement.getOriginalDocumentNo())
|
||||
|| measurement.getOriginalGrossWeightKg() == null || measurement.getOriginalVolumeCbm() == null
|
||||
|| !StringUtils.hasText(measurement.getActualGoodsName());
|
||||
}
|
||||
|
||||
private OrderCargoClient.MeasurementDeclarationSnapshot loadDeclaration(InboundOrderEntity inbound) {
|
||||
if (inbound.getOrderId() == null || inbound.getOrderId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单未关联有效订单,无法读取客户申报数据");
|
||||
}
|
||||
try {
|
||||
ApiResponse<OrderCargoClient.MeasurementDeclarationSnapshot> response = orderCargoClient
|
||||
.measurementDeclaration(internalToken, inbound.getTenantId(), inbound.getOrderId());
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
String message = response == null || !StringUtils.hasText(response.message())
|
||||
? "客户订单申报数据查询失败" : response.message();
|
||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||
: ResultCode.of(response.code()), message);
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法读取客户申报数据");
|
||||
}
|
||||
}
|
||||
|
||||
private String defaultGoodsName(OrderCargoClient.MeasurementDeclarationSnapshot declaration) {
|
||||
String value = declaration.defaultActualGoodsName();
|
||||
if (!StringUtils.hasText(value)) return null;
|
||||
if (value.length() > 255) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"客户货物品名汇总超过255字符,请填写精简后的实际品名");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) {
|
||||
return new InboundMeasurementResponses.Item(
|
||||
item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
|
||||
@@ -28,6 +28,8 @@ public class MeasurementReviewService {
|
||||
@Resource
|
||||
private InboundWorkflowRepository repository;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.repository.InboundMeasurementRepository measurements;
|
||||
@Resource
|
||||
private InboundIdempotencyService idempotency;
|
||||
@Resource
|
||||
private InboundProperties properties;
|
||||
@@ -78,12 +80,30 @@ public class MeasurementReviewService {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"人工确认箱数合计与入库单总箱数不一致,不能审核通过");
|
||||
}
|
||||
BigDecimal weight = order.getActualGrossWeightKg();
|
||||
var weights = measurements.listWeights(tenantId, inboundId);
|
||||
if (weights.isEmpty() || weights.stream().anyMatch(weight ->
|
||||
!"CONFIRMED".equals(weight.getRecognitionStatus())
|
||||
|| weight.getGrossWeightKg() == null
|
||||
|| weight.getGrossWeightKg().signum() <= 0
|
||||
|| weight.getNetWeightKg() == null
|
||||
|| weight.getNetWeightKg().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"所有称重结果必须已确认且包含大于零的毛重和货物净重,才能审核通过");
|
||||
}
|
||||
BigDecimal grossWeight = weights.stream()
|
||||
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getGrossWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal netWeight = weights.stream()
|
||||
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getNetWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm)
|
||||
.filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (weight == null || weight.signum() <= 0 || volume.signum() <= 0) {
|
||||
if (order.getActualGrossWeightKg() == null || order.getActualNetWeightKg() == null
|
||||
|| order.getActualGrossWeightKg().compareTo(grossWeight) != 0
|
||||
|| order.getActualNetWeightKg().compareTo(netWeight) != 0
|
||||
|| volume.signum() <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"审核通过的测量快照必须包含大于零的货物总重量和体积");
|
||||
"审核通过前称重毛重、货物净重汇总和体积必须完整且一致");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ public class MerchantInboundBillingService {
|
||||
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
||||
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
||||
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
||||
measurement.actualNetWeightKg(),
|
||||
measurement.actualVolumeCbm(), measurement.volumeWeightKg(), measurement.actualGoodsName());
|
||||
var measurementImages = measurement.items().stream()
|
||||
.filter(item -> item.intelligentImageFileId() != null)
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.baiyue.firstleg.dto.UnifiedInboundDetail;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundSummary;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
|
||||
import com.baiyue.firstleg.dto.CurrentContainerTransport;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
||||
import com.baiyue.firstleg.repository.UnifiedInboundReadRepository;
|
||||
@@ -75,7 +75,7 @@ public class UnifiedInboundQueryService {
|
||||
evidence = operations.findEvidence(tenantId, inbound.getId());
|
||||
deliveryAt = deliveryAt(detail.statusHistory());
|
||||
}
|
||||
InboundTransportSnapshotEntity transport = includePhaseThree
|
||||
CurrentContainerTransport transport = includePhaseThree
|
||||
? operations.findTransport(tenantId, inbound.getId()) : null;
|
||||
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt,
|
||||
transport, null, null), timeline);
|
||||
@@ -105,7 +105,7 @@ public class UnifiedInboundQueryService {
|
||||
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload,
|
||||
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick,
|
||||
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
|
||||
InboundTransportSnapshotEntity transport, String locationCodes,
|
||||
CurrentContainerTransport transport, String locationCodes,
|
||||
com.baiyue.firstleg.entity.InboundPickupSnapshotEntity pickup) {
|
||||
String mainStatus = mainStatus(inbound.getProcessStatus());
|
||||
List<LoadingEvidenceEntity> activeEvidence = evidence.stream()
|
||||
@@ -124,13 +124,22 @@ public class UnifiedInboundQueryService {
|
||||
? new UnifiedInboundSummary.DeliverySummary(
|
||||
"DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt,
|
||||
inbound.getExpressTrackingNo()) : null;
|
||||
var inboundSummary = detail == null
|
||||
? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, pickup, locationCodes) : detail.summary();
|
||||
var baseSummary = detail == null ? null : detail.summary();
|
||||
var inboundSummary = com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport,
|
||||
pickup == null && baseSummary != null && baseSummary.requestedPickupAt() != null
|
||||
? pickupSnapshot(baseSummary.requestedPickupAt()) : pickup,
|
||||
locationCodes == null && baseSummary != null ? baseSummary.locationCodes() : locationCodes);
|
||||
return new UnifiedInboundSummary(inboundSummary, mainStatus,
|
||||
nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()),
|
||||
preloadSummary, loadingSummary, deliverySummary);
|
||||
}
|
||||
|
||||
private com.baiyue.firstleg.entity.InboundPickupSnapshotEntity pickupSnapshot(LocalDateTime pickupAt) {
|
||||
var snapshot = new com.baiyue.firstleg.entity.InboundPickupSnapshotEntity();
|
||||
snapshot.setPickupAt(pickupAt);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private LocalDateTime deliveryAt(List<InboundOrderDetail.StatusHistorySummary> history) {
|
||||
return history.stream()
|
||||
.filter(item -> "COMPLETE_DELIVERY".equals(item.action()) || "DELIVERED".equals(item.toStatus()))
|
||||
|
||||
@@ -42,13 +42,13 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
||||
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
||||
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '下游货物实际毛重kg,汇总已确认称重净重',
|
||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片含托盘毛重汇总kg',
|
||||
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
||||
actual_volume_cbm DECIMAL(18,4) NULL COMMENT '确认后的实际总体积m3',
|
||||
chargeable_volume_cbm DECIMAL(18,4) NULL COMMENT '当前费用快照的计费体积m3',
|
||||
pallet_count INT UNSIGNED NULL COMMENT '已分配总托盘数快照',
|
||||
volume_weight_kg DECIMAL(18,3) NULL COMMENT '总材积重kg',
|
||||
actual_net_weight_kg DECIMAL(18,3) NULL COMMENT '总净重kg',
|
||||
actual_net_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片扣除托盘皮重后的货物净重汇总kg',
|
||||
measure_completed_at DATETIME(3) NULL COMMENT '测量完成时间',
|
||||
inbounded_at DATETIME(3) NULL COMMENT '入库完成时间',
|
||||
express_company_code VARCHAR(64) NULL COMMENT '快递公司编码快照',
|
||||
|
||||
@@ -566,8 +566,8 @@ paths:
|
||||
由稳定的 item.id(measurementItemId)与 intelligentImageFileId 关联;称重图片只随
|
||||
weightResults 返回,由稳定的 weightResult.id 与 fileId 关联,并携带确认状态及服务端确认的
|
||||
grossWeightKg/netWeightKg。两类证据不得合并为无明细归属的统一图片列表。
|
||||
actualGrossWeightKg 是服务端权威的货物总重量;存在已确认称重结果时为所有已确认
|
||||
netWeightKg 之和,消费者不得根据图片或 OCR 值自行重算。
|
||||
actualGrossWeightKg 是服务端确认后的实测毛重,actualNetWeightKg 是扣除托盘皮重后的货物净重;
|
||||
两者语义独立,消费者不得互相回退,也不得根据图片或 OCR 值自行重算。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -578,6 +578,23 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/InboundMeasurementDetailEnvelope'}
|
||||
'403': {description: 无入库测量查看权限}
|
||||
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/draft:
|
||||
put:
|
||||
summary: 保存测量草稿
|
||||
description: >-
|
||||
原单据号码、申报毛重和申报体积由服务端从客户订单生成快照,客户端不得提交覆盖;
|
||||
actualGoodsName 默认使用客户货物品名汇总,但允许测量人员修正。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/InboundMeasurementSaveDraftRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 无测量编辑权限}
|
||||
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:
|
||||
post:
|
||||
summary: 平台模拟单箱设备扫描
|
||||
@@ -697,7 +714,7 @@ paths:
|
||||
platform:firstleg:inbound:weight:edit 权限。修改与确认在同一事务内完成,保留 OCR 原值,
|
||||
分别写入 CORRECT 和 CONFIRM 修订记录。输入表示含托盘毛重;服务端统一换算为 KG,
|
||||
使用创建时固化的托盘皮重计算 netWeightKg,且毛重必须大于皮重。
|
||||
入库单 actualGrossWeightKg 明确保留为下游货物实际毛重字段,值汇总所有已确认 netWeightKg。
|
||||
入库单 actualGrossWeightKg 为实测毛重;actualNetWeightKg 为扣除托盘皮重后的货物净重,两者不得混用。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -749,7 +766,7 @@ paths:
|
||||
summary: 删除称重图片及重量结果
|
||||
description: >-
|
||||
需要 platform:firstleg:inbound:weight:edit 权限;仅允许在可编辑的测量状态删除。
|
||||
删除已确认图片后重新汇总剩余已确认图片的净重,删除最后一张时 actualGrossWeightKg 恢复为空。
|
||||
删除已确认图片后由生产者分别重算剩余确认称重的毛重、净重;消费端不得用净重填充毛重。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: version, in: query, required: true, schema: {type: integer, format: int32, minimum: 0}}
|
||||
@@ -779,15 +796,6 @@ paths:
|
||||
reason: {type: string, maxLength: 1000}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断;审核成功 data.processStatus 固定为 WAIT_SPLIT,initialPricingSucceeded 表示是否同时生成初算费用,初算失败时 feeStatus=UNPRICED 且主流程仍成功}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/reject:
|
||||
post:
|
||||
summary: 驳回测量
|
||||
description: 需要 firstleg:inbound:measurement-review 权限;只退回测量数据修改,已确认托盘和库位占用保持不动。
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/submit:
|
||||
post:
|
||||
summary: 提交测量完成
|
||||
@@ -811,15 +819,6 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/MerchantInboundBillingEnvelope'}
|
||||
'403': {description: 非当前客户或无订单查看权限}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/exception-release:
|
||||
post:
|
||||
summary: 测量异常放行
|
||||
description: 需要 firstleg:inbound:exception-release 权限
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败}
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -1035,8 +1034,8 @@ components:
|
||||
currentHoldReason: {type: string, nullable: true}
|
||||
totalPackageCount: {type: integer, nullable: true}
|
||||
locationCodes: {type: string, nullable: true}
|
||||
actualGrossWeightKg: {type: number, nullable: true, multipleOf: 0.001, description: 服务端权威货物总重量 KG;存在已确认称重图片时为全部 CONFIRMED netWeightKg 之和,消费者不得从图片重算}
|
||||
actualNetWeightKg: {type: number, nullable: true}
|
||||
actualGrossWeightKg: {type: number, nullable: true, multipleOf: 0.001, description: 服务端确认后的实测毛重 KG;只读取 fl_inbound_order.actual_gross_weight_kg,不以净重回退}
|
||||
actualNetWeightKg: {type: number, nullable: true, multipleOf: 0.001, description: 服务端确认后的货物净重 KG;只读取 fl_inbound_order.actual_net_weight_kg,不以毛重回退}
|
||||
actualVolumeCbm: {type: number, nullable: true}
|
||||
volumeWeightKg: {type: number, nullable: true}
|
||||
chargeableWeightKg: {type: number, nullable: true}
|
||||
@@ -1050,8 +1049,10 @@ components:
|
||||
cabinetNo: {type: string, nullable: true}
|
||||
vesselName: {type: string, nullable: true}
|
||||
voyageNo: {type: string, nullable: true}
|
||||
sailingAt: {type: string, format: date-time, nullable: true}
|
||||
portArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
estimatedSailingAt: {type: string, format: date-time, nullable: true, description: 当前关联柜次预计开船时间}
|
||||
actualSailingAt: {type: string, format: date-time, nullable: true, description: 当前关联柜次实际开船时间}
|
||||
estimatedPortArrivalAt: {type: string, format: date-time, nullable: true, description: 当前关联柜次预计到港时间}
|
||||
actualPortArrivalAt: {type: string, format: date-time, nullable: true, description: 当前关联柜次实际到港时间}
|
||||
expressTrackingNo: {type: string, nullable: true}
|
||||
pickupMethod: {type: string, enum: [SELF_EXPRESS, PICKUP, SELF_TRUCK], nullable: true}
|
||||
requestedPickupAt: {type: string, format: date-time, nullable: true, description: 客户要求平台上门提货的预约时间}
|
||||
@@ -1286,9 +1287,9 @@ components:
|
||||
measurementStatus: {type: string, nullable: true}
|
||||
inboundVersion: {type: integer, minimum: 0}
|
||||
version: {type: integer, minimum: 0, nullable: true}
|
||||
originalDocumentNo: {type: string, nullable: true}
|
||||
originalGrossWeightKg: {type: number, format: double, nullable: true}
|
||||
originalVolumeCbm: {type: number, format: double, nullable: true}
|
||||
originalDocumentNo: {type: string, nullable: true, description: 客户订单 referenceId 的服务端快照;旧空快照按订单事实回退}
|
||||
originalGrossWeightKg: {type: number, format: double, nullable: true, description: 客户订单有效货物行 grossWeightKg 合计的服务端快照}
|
||||
originalVolumeCbm: {type: number, format: double, nullable: true, description: 客户订单有效货物行 volumeCbm 合计的服务端快照}
|
||||
originalPackageCount: {type: integer, nullable: true}
|
||||
actualPackageCount: {type: integer, nullable: true}
|
||||
actualGrossWeightKg:
|
||||
@@ -1296,10 +1297,16 @@ components:
|
||||
format: double
|
||||
nullable: true
|
||||
multipleOf: 0.001
|
||||
description: 服务端权威货物总重量 KG;存在已确认称重结果时为所有 CONFIRMED netWeightKg 之和
|
||||
description: 全部 CONFIRMED 称重结果 grossWeightKg 之和,含托盘毛重 KG;无确认结果时为空
|
||||
actualNetWeightKg:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
multipleOf: 0.001
|
||||
description: 全部 CONFIRMED 称重结果 netWeightKg 之和,扣除托盘皮重后的货物净重 KG;无确认结果时为空,审核与计费以此为准
|
||||
actualVolumeCbm: {type: number, format: double, nullable: true}
|
||||
volumeWeightKg: {type: number, format: double, nullable: true}
|
||||
actualGoodsName: {type: string, nullable: true}
|
||||
actualGoodsName: {type: string, nullable: true, description: 默认由客户货物品名去重汇总,测量阶段允许人工修正}
|
||||
items:
|
||||
type: array
|
||||
description: DIMENSION 尺寸测量明细及其图片证据
|
||||
@@ -1308,6 +1315,38 @@ components:
|
||||
type: array
|
||||
description: WEIGHT 称重结果及其图片、确认状态和服务端重量事实
|
||||
items: {$ref: '#/components/schemas/InboundWeightResult'}
|
||||
InboundMeasurementSaveDraftRequest:
|
||||
type: object
|
||||
required: [version, items]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
measurementId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
actualGoodsName: {type: string, maxLength: 255, nullable: true}
|
||||
items:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 500
|
||||
items: {$ref: '#/components/schemas/InboundMeasurementItemRequest'}
|
||||
InboundMeasurementItemRequest:
|
||||
type: object
|
||||
required: [inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm, unitWeightKg]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
inboundDetailId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
itemNo: {type: string, minLength: 1, maxLength: 32}
|
||||
boxSpecification: {type: string, minLength: 1, maxLength: 64}
|
||||
packageCount: {type: integer, minimum: 1, maximum: 1000000}
|
||||
lengthCm: {type: number, minimum: 0.001}
|
||||
widthCm: {type: number, minimum: 0.001}
|
||||
heightCm: {type: number, minimum: 0.001}
|
||||
unitWeightKg: {type: number, minimum: 0.001}
|
||||
intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
overLimitWarning: {type: string, maxLength: 500, nullable: true}
|
||||
outerLengthCm: {type: number, minimum: 0, nullable: true}
|
||||
outerWidthCm: {type: number, minimum: 0, nullable: true}
|
||||
outerHeightCm: {type: number, minimum: 0, nullable: true}
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
InboundMeasurementDetailEnvelope:
|
||||
type: object
|
||||
required: [code, message]
|
||||
|
||||
Reference in New Issue
Block a user