优化
This commit is contained in:
@@ -5,6 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/** 仅用于确认证据文件属于当前客户和入库单,不在 firstleg 复制文件元数据。 */
|
||||
@FeignClient(name = "file-service", contextId = "firstlegLoadingEvidenceFileClient",
|
||||
@@ -15,5 +16,10 @@ public interface LoadingEvidenceFileClient {
|
||||
@PathVariable long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long fileId);
|
||||
|
||||
@GetMapping("/{tenantId}/{inboundId}/{fileId}/content")
|
||||
ResponseEntity<byte[]> download(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long fileId);
|
||||
|
||||
record FileMetadata(Long fileId, String fileName, String businessType, String contentType) {}
|
||||
}
|
||||
|
||||
+3
-3
@@ -31,12 +31,12 @@ public class InternalPriceBookController {
|
||||
}
|
||||
|
||||
@PostMapping("/channels/validate")
|
||||
public ApiResponse<Void> validateChannel(
|
||||
public ApiResponse<PriceBookChannelValidation.Response> validateChannel(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody PriceBookChannelValidation.Request request) {
|
||||
requireToken(token);
|
||||
service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(), request.channelCode());
|
||||
return ApiResponse.success(null);
|
||||
return ApiResponse.success(service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(),
|
||||
request.channelCode(), request.destinationType()));
|
||||
}
|
||||
|
||||
private void requireToken(String token) {
|
||||
|
||||
+12
@@ -55,6 +55,18 @@ public class PlatformContainerController {
|
||||
public ApiResponse<Overview> overview(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(overview.overview(id));
|
||||
}
|
||||
@GetMapping("/{id}/pda/photos/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ResponseEntity<byte[]> downloadPdaPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@PathVariable @Positive long fileId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
var value = overview.downloadPhoto(id, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, org.springframework.http.ContentDisposition.inline()
|
||||
.filename(value.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(value.contentType()).contentLength(value.content().length).body(value.content());
|
||||
}
|
||||
@PostMapping("/{id}/exception") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
public ApiResponse<Void> markException(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public class PlatformFbaPricingController {
|
||||
@PutMapping("/published")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:adjust')")
|
||||
@BusinessAudit(event = "FBA_FEE_ADJUST", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "调整已发布入库费用,不回退入库状态")
|
||||
description = "调整初算或已发布入库费用,不回退待分柜状态")
|
||||
public ApiResponse<FeeSnapshotResponse> adjustPublished(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
|
||||
+8
-11
@@ -8,11 +8,11 @@ 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;
|
||||
import com.baiyue.firstleg.service.MeasurementReviewService;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import com.baiyue.firstleg.service.InitialInboundPricingService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -36,15 +36,15 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/v1/inbounds")
|
||||
public class PlatformInboundMeasurementReviewController {
|
||||
@Resource private MeasurementReviewService service;
|
||||
@Resource private AutomaticInboundCompletionService automaticCompletion;
|
||||
@Resource private InitialInboundPricingService initialPricing;
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_APPROVE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量审核通过")
|
||||
@Operation(summary = "测量审核通过",
|
||||
description = "客服确认测量数据后直接进入待分柜;费用和其他资料可在后续继续维护。")
|
||||
description = "客服确认测量数据后直接进入待分柜,并按已审核测量数据尝试生成初算费用;初算失败不阻塞状态流转。")
|
||||
@PostMapping("/{inboundId}/measurements/approve")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review')")
|
||||
public ApiResponse<FeeSnapshotResponse> approve(
|
||||
public ApiResponse<MeasurementApprovalResponse> approve(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@@ -52,12 +52,9 @@ public class PlatformInboundMeasurementReviewController {
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
// 审核服务事务在此调用返回时已提交;随后自动编排从新事务/新查询读取 WAIT_PRICING,
|
||||
// 避免在未提交的审核事务中读取旧状态。自动流程失败时保留已审核状态,原样返回业务错误,
|
||||
// 使用同一幂等键重试会从持久化阶段继续推进。
|
||||
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
return ApiResponse.success(automaticCompletion.completeApproved(
|
||||
tenantId, inboundId, user.userId(), user.username(), request.measurementId(), request.expectedVersion() + 1));
|
||||
return ApiResponse.success(initialPricing.tryCalculate(tenantId, user.userId(), inboundId,
|
||||
request.measurementId(), request.expectedVersion() + 1));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_REJECT", resourceType = "INBOUND",
|
||||
@@ -80,7 +77,7 @@ public class PlatformInboundMeasurementReviewController {
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_EXCEPTION_RELEASE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量驳回异常放行")
|
||||
@Operation(summary = "测量驳回异常放行",
|
||||
description = "Nacos 开关开启、有异常放行权限且原因非空时,才可从 MEASURE_REJECTED 推进至 WAIT_PRICING。")
|
||||
description = "Nacos 开关开启、有异常放行权限且原因非空时,才可从 MEASURE_REJECTED 推进至 WAIT_SPLIT。")
|
||||
@PostMapping("/{inboundId}/measurements/exception-release")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:exception-release')")
|
||||
public ApiResponse<Void> exceptionRelease(
|
||||
|
||||
@@ -8,29 +8,33 @@ public final class PriceBookChannelCatalog {
|
||||
private PriceBookChannelCatalog() {}
|
||||
|
||||
public static final List<ChannelDefinition> CHANNELS = List.of(
|
||||
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡"),
|
||||
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡"),
|
||||
fba("EXX_TRUCK", "EXX海卡", "EXX海卡"),
|
||||
fba("HEDE_TRUCK", "合德海卡", "合德海卡"),
|
||||
fba("EXX_RAIL_NY", "纽约EXX海铁", "纽约EXX海铁"),
|
||||
fba("OA_LA_TRUCK", "洛杉矶普船海卡", "洛杉矶普船海卡"),
|
||||
fba("OA_LA_FULL_TRUCK", "洛杉矶普船全卡", "洛杉矶普船全卡"),
|
||||
fba("OA_CHICAGO_RAIL", "芝加哥普船海铁", "芝加哥普船海铁"),
|
||||
fba("OA_HOUSTON_TRUCK", "休斯顿普船海卡", "休斯顿普船海卡"),
|
||||
fba("OA_NEW_YORK_TRUCK", "纽约普船海卡", "纽约普船海卡"),
|
||||
fba("OA_SAVANNAH_TRUCK", "萨凡纳普船海卡", "萨凡纳普船海卡"),
|
||||
parcel("MATSON_CLX_PARCEL", "美森正班海派", "海派"),
|
||||
parcel("MATSON_MAX_PARCEL", "美森加班海派", "海派"),
|
||||
parcel("HEDE_PARCEL", "合德海派", "海派"),
|
||||
parcel("OA_PARCEL", "普船海派", "海派")
|
||||
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡", "ZBYHK", "ZBYHWK"),
|
||||
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡", "BYHK", "BYHWK"),
|
||||
fba("EXX_TRUCK", "EXX海卡", "EXX海卡", "EXXHK", "EXXHWK"),
|
||||
fba("HEDE_TRUCK", "合德海卡", "合德海卡", "HDHK", "HDHWK"),
|
||||
fba("EXX_RAIL_NY", "纽约EXX海铁", "纽约EXX海铁", "NYEXX", null),
|
||||
fba("OA_LA_TRUCK", "洛杉矶普船海卡", "洛杉矶普船海卡", "PCHK", "PCHWK"),
|
||||
fba("OA_LA_FULL_TRUCK", "洛杉矶普船全卡", "洛杉矶普船全卡", "PCKK", null),
|
||||
fba("OA_CHICAGO_RAIL", "芝加哥普船海铁", "芝加哥普船海铁", "CPHT", null),
|
||||
fba("OA_HOUSTON_TRUCK", "休斯顿普船海卡", "休斯顿普船海卡", "HPHK", null),
|
||||
fba("OA_NEW_YORK_TRUCK", "纽约普船海卡", "纽约普船海卡", "NPHK", "NPHWK"),
|
||||
fba("OA_SAVANNAH_TRUCK", "萨凡纳普船海卡", "萨凡纳普船海卡", "SPHK", null),
|
||||
parcel("MATSON_CLX_PARCEL", "美森正班海派", "海派", "ZBYHP", null),
|
||||
parcel("MATSON_MAX_PARCEL", "美森加班海派", "海派", "BYHP", null),
|
||||
parcel("HEDE_PARCEL", "合德海派", "海派", "HDHP", null),
|
||||
parcel("OA_PARCEL", "普船海派", "海派", "PCHP", null)
|
||||
);
|
||||
|
||||
private static ChannelDefinition fba(String code, String name, String sheet) {
|
||||
return new ChannelDefinition(code, name, PriceModel.FBA_WAREHOUSE_RATE, sheet, Set.of(name));
|
||||
private static ChannelDefinition fba(String code, String name, String sheet,
|
||||
String inboundCodePrefix, String overseasInboundCodePrefix) {
|
||||
return new ChannelDefinition(code, name, PriceModel.FBA_WAREHOUSE_RATE, sheet, Set.of(name),
|
||||
inboundCodePrefix, overseasInboundCodePrefix);
|
||||
}
|
||||
|
||||
private static ChannelDefinition parcel(String code, String name, String sheet) {
|
||||
return new ChannelDefinition(code, name, PriceModel.REGION_WEIGHT_TIER, sheet, Set.of(name));
|
||||
private static ChannelDefinition parcel(String code, String name, String sheet,
|
||||
String inboundCodePrefix, String overseasInboundCodePrefix) {
|
||||
return new ChannelDefinition(code, name, PriceModel.REGION_WEIGHT_TIER, sheet, Set.of(name),
|
||||
inboundCodePrefix, overseasInboundCodePrefix);
|
||||
}
|
||||
|
||||
public record ChannelDefinition(
|
||||
@@ -38,6 +42,8 @@ public final class PriceBookChannelCatalog {
|
||||
String displayName,
|
||||
PriceModel priceModel,
|
||||
String sourceSheetName,
|
||||
Set<String> aliases
|
||||
Set<String> aliases,
|
||||
String inboundCodePrefix,
|
||||
String overseasInboundCodePrefix
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public final class ContainerOverviewDtos {
|
||||
String status, Long warehouseId, String warehouseName, String voyageNo, String vesselName,
|
||||
Integer version) {}
|
||||
public record PdaSummary(Long taskId, Long loadingPlanId, Long containerBookingId, String taskStatus,
|
||||
Integer taskVersion, Integer itemCount, Integer completedItemCount, Integer unresolvedItemCount,
|
||||
String workflowStatus, Integer taskVersion, Integer itemCount, Integer completedItemCount, Integer unresolvedItemCount,
|
||||
List<PhotoCount> photoCounts, List<Photo> photos, List<String> exceptionReasons) {}
|
||||
public record PhotoCount(String evidenceType, Integer count) {}
|
||||
public record Photo(Long evidenceId, Long tenantId, Long inboundId, String evidenceType,
|
||||
|
||||
@@ -6,6 +6,7 @@ import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -52,7 +53,9 @@ public final class FbaPricingDtos {
|
||||
public record CalculateRequest(LocalDateTime pricingAt) {}
|
||||
|
||||
public record AdjustFeeRequest(
|
||||
@NotNull @DecimalMin("0.00") BigDecimal subtotalAmount,
|
||||
@NotNull @Positive Integer expectedFeeVersion,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal chargeableQuantity,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal additionalAmount,
|
||||
@NotBlank @Size(max = 500) String reason
|
||||
) {}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
|
||||
public record MeasurementApprovalResponse(
|
||||
Long inboundId,
|
||||
Long measurementId,
|
||||
String processStatus,
|
||||
String feeStatus,
|
||||
boolean initialPricingSucceeded,
|
||||
String initialPricingMessage,
|
||||
FeeSnapshotResponse initialFee
|
||||
) {}
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.dto;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public final class PriceBookChannelValidation {
|
||||
private PriceBookChannelValidation() {}
|
||||
@@ -10,6 +11,9 @@ public final class PriceBookChannelValidation {
|
||||
public record Request(
|
||||
@Min(1) long tenantId,
|
||||
@Min(1) long originWarehouseId,
|
||||
@NotBlank @Size(max = 64) String channelCode
|
||||
@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotBlank @Pattern(regexp = "FBA|OVERSEAS|PRIVATE") String destinationType
|
||||
) {}
|
||||
|
||||
public record Response(String channelCode, String inboundCodePrefix) {}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,7 @@ public class PriceChannelCatalogEntity {
|
||||
private String priceModel;
|
||||
private String sourceSheetName;
|
||||
private String aliasesJson;
|
||||
private String inboundCodePrefix;
|
||||
private String overseasInboundCodePrefix;
|
||||
private String status;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.entity;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import java.math.BigDecimal;
|
||||
@@ -53,4 +54,12 @@ public class TruckAppointmentEntity {
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
@TableField(exist = false) private String oceanBookingNo;
|
||||
@TableField(exist = false) private String warehouseName;
|
||||
@TableField(exist = false) private Long shippingCompanyId;
|
||||
@TableField(exist = false) private String shippingCompanyName;
|
||||
@TableField(exist = false) private Long bookingAgentId;
|
||||
@TableField(exist = false) private String bookingAgentName;
|
||||
@TableField(exist = false) private String cabinetNo;
|
||||
@TableField(exist = false) private String containerNo;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update;
|
||||
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'WAIT_PRICING', measure_review_status = 'APPROVED',
|
||||
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
||||
actual_gross_weight_kg = (SELECT COALESCE(SUM(item.total_weight_kg), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
|
||||
@@ -21,7 +21,7 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
volume_weight_kg = (SELECT COALESCE(SUM(item.volume_weight_kg), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
|
||||
updated_by = #{operatorId}, version = version + 1
|
||||
inbounded_at = CURRENT_TIMESTAMP(3), updated_by = #{operatorId}, version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
|
||||
AND process_status = 'MEASURE_PENDING' AND measure_review_status = 'PENDING'
|
||||
AND version = #{expectedVersion}
|
||||
@@ -45,7 +45,8 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'WAIT_PRICING', updated_by = #{operatorId}, version = version + 1
|
||||
SET process_status = 'WAIT_SPLIT', inbounded_at = CURRENT_TIMESTAMP(3),
|
||||
updated_by = #{operatorId}, version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
|
||||
AND process_status = 'MEASURE_REJECTED' AND measure_review_status = 'REJECTED'
|
||||
AND version = #{expectedVersion}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.mapper;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelPrefixRow;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@@ -12,6 +13,15 @@ import java.util.List;
|
||||
/** Dedicated read model for merchant channel selection and reference estimates. */
|
||||
@Mapper
|
||||
public interface PriceBookQueryMapper {
|
||||
@Select("""
|
||||
SELECT channel_code AS channelCode, inbound_code_prefix AS inboundCodePrefix,
|
||||
overseas_inbound_code_prefix AS overseasInboundCodePrefix
|
||||
FROM fl_price_channel_catalog
|
||||
WHERE channel_code = #{channelCode} AND status = 'ACTIVE'
|
||||
LIMIT 1
|
||||
""")
|
||||
ChannelPrefixRow findChannelPrefix(@Param("channelCode") String channelCode);
|
||||
|
||||
@Select("""
|
||||
SELECT id
|
||||
FROM fl_price_book_version
|
||||
|
||||
@@ -192,15 +192,30 @@ public class FbaPricingRepository {
|
||||
.orderByAsc(InboundFeeItemEntity::getItemNo));
|
||||
}
|
||||
|
||||
public int supersedeSent(long tenantId, long feeId) {
|
||||
public int supersedeCurrent(long tenantId, long feeId, String feeStatus) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
.eq(InboundFeeEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeEntity::getId, feeId)
|
||||
.eq(InboundFeeEntity::getFeeStatus, "SENT")
|
||||
.eq(InboundFeeEntity::getFeeStatus, feeStatus)
|
||||
.set(InboundFeeEntity::getFeeStatus, "SUPERSEDED")
|
||||
.set(InboundFeeEntity::getUpdatedAt, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
public int markInboundAdjusted(long tenantId, long inboundId, String feeStatus,
|
||||
String chargeUnit, java.math.BigDecimal chargeableQuantity) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId)
|
||||
.eq(InboundPricingEntity::getId, inboundId)
|
||||
.eq(InboundPricingEntity::getProcessStatus, "WAIT_SPLIT")
|
||||
.set(InboundPricingEntity::getFeeStatus, feeStatus);
|
||||
if ("KG".equals(chargeUnit)) {
|
||||
update.set(InboundPricingEntity::getChargeableWeightKg, chargeableQuantity);
|
||||
} else {
|
||||
update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeableQuantity);
|
||||
}
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode,
|
||||
java.math.BigDecimal chargeable) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
|
||||
@@ -46,4 +46,8 @@ public class PriceBookQueryRepository {
|
||||
return channels(versionId, originWarehouseId).stream()
|
||||
.anyMatch(channel -> channelCode.equals(channel.getChannelCode()));
|
||||
}
|
||||
|
||||
public PriceBookQueryRows.ChannelPrefixRow channelPrefix(String channelCode) {
|
||||
return mapper.findChannelPrefix(channelCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.time.LocalDate;
|
||||
public final class PriceBookQueryRows {
|
||||
private PriceBookQueryRows() {}
|
||||
|
||||
public record ChannelPrefixRow(String channelCode, String inboundCodePrefix,
|
||||
String overseasInboundCodePrefix) {}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ChannelRow {
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AutomaticInboundCompletionService {
|
||||
@Resource
|
||||
private FbaPricingService pricing;
|
||||
@Resource
|
||||
private InboundWorkflowRepository workflow;
|
||||
@Resource
|
||||
private InboundCompletionOrchestrator completion;
|
||||
|
||||
@BusinessAudit(event = "INBOUND_AUTO_COMPLETE", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "测量审核通过后自动计费、发送费用并完成入库", captureBefore = false)
|
||||
public FeeSnapshotResponse completeApproved(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, long measurementId,
|
||||
int reviewedOrderVersion) {
|
||||
String key = "auto-" + inboundId + "-" + measurementId + "-" + reviewedOrderVersion;
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
if (completed(order)) return pricing.latest(tenantId, inboundId);
|
||||
|
||||
if ("WAIT_PRICING".equals(order.getProcessStatus()) && "UNPRICED".equals(order.getFeeStatus())) {
|
||||
pricing.calculate(tenantId, operatorId, inboundId, null, key + "-calculate");
|
||||
order = requireOrder(tenantId, inboundId);
|
||||
}
|
||||
if ("FEE_CONFIRMING".equals(order.getProcessStatus())
|
||||
&& "CALCULATED".equals(order.getFeeStatus())) {
|
||||
pricing.confirm(tenantId, operatorId, inboundId, key + "-confirm");
|
||||
order = requireOrder(tenantId, inboundId);
|
||||
}
|
||||
|
||||
FeeSnapshotResponse fee = pricing.latest(tenantId, inboundId);
|
||||
if ("FEE_CONFIRMING".equals(order.getProcessStatus())
|
||||
&& "CONFIRMED".equals(order.getFeeStatus())) {
|
||||
completion.beginSendFeeAndInbound(tenantId, operatorId, operatorName, inboundId,
|
||||
fee.feeId(), order.getVersion(), key + "-begin");
|
||||
order = requireOrder(tenantId, inboundId);
|
||||
}
|
||||
if ("INBOUNDING".equals(order.getProcessStatus())
|
||||
&& "CONFIRMED".equals(order.getFeeStatus())) {
|
||||
completion.completeSendFeeAndInbound(tenantId, operatorId, operatorName, inboundId,
|
||||
fee.feeId(), order.getVersion(), key + "-complete");
|
||||
order = requireOrder(tenantId, inboundId);
|
||||
}
|
||||
if (!completed(order)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客服审核后的自动计费入库流程状态异常");
|
||||
}
|
||||
return pricing.latest(tenantId, inboundId);
|
||||
}
|
||||
|
||||
private static boolean completed(InboundOrderEntity order) {
|
||||
return "WAIT_SPLIT".equals(order.getProcessStatus())
|
||||
&& "SENT".equals(order.getFeeStatus());
|
||||
}
|
||||
|
||||
private InboundOrderEntity requireOrder(long tenantId, long inboundId) {
|
||||
InboundOrderEntity order = workflow.findOwned(tenantId, inboundId);
|
||||
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,14 @@ import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerOverviewMapper;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.client.LoadingEvidenceFileClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import feign.FeignException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
@@ -25,6 +29,8 @@ import java.util.List;
|
||||
public class ContainerOverviewService {
|
||||
@Resource private ContainerRepository containers;
|
||||
@Resource private ContainerOverviewMapper mapper;
|
||||
@Resource private LoadingEvidenceFileClient evidenceFiles;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
|
||||
public Overview overview(long id) {
|
||||
ContainerBookingEntity booking = required(id);
|
||||
@@ -94,18 +100,47 @@ public class ContainerOverviewService {
|
||||
private PdaSummary pda(ContainerOverviewMapper.PickTaskRow task) {
|
||||
List<Photo> photos = mapper.photos(task.loadingPlanId()).stream().map(value -> new Photo(value.evidenceId(),
|
||||
value.tenantId(), value.inboundId(), value.evidenceType(), value.fileId(), value.fileName(),
|
||||
value.status(), "/api/platform/v1/files/loading-evidence/" + value.tenantId() + '/'
|
||||
+ value.inboundId() + '/' + value.fileId(), value.uploadedAt())).toList();
|
||||
value.status(), "/api/platform/v1/containers/" + task.containerBookingId() + "/pda/photos/"
|
||||
+ value.fileId() + "/download", value.uploadedAt())).toList();
|
||||
var counts = new LinkedHashMap<String, Integer>();
|
||||
photos.stream().filter(photo -> "ACTIVE".equals(photo.status()))
|
||||
.forEach(photo -> counts.merge(photo.evidenceType(), 1, Integer::sum));
|
||||
List<PhotoCount> photoCounts = counts.entrySet().stream()
|
||||
.map(entry -> new PhotoCount(entry.getKey(), entry.getValue())).toList();
|
||||
int itemCount = mapper.itemCount(task.id());
|
||||
int unresolvedItemCount = mapper.unresolvedItemCount(task.id());
|
||||
String workflowStatus = "COMPLETED".equals(task.taskStatus()) ? "COMPLETED"
|
||||
: unresolvedItemCount == 0 ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
||||
return new PdaSummary(task.id(), task.loadingPlanId(), task.containerBookingId(), task.taskStatus(),
|
||||
task.version(), mapper.itemCount(task.id()), mapper.completedItemCount(task.id()),
|
||||
mapper.unresolvedItemCount(task.id()), photoCounts, photos, mapper.pickExceptionReasons(task.id()));
|
||||
workflowStatus, task.version(), itemCount, mapper.completedItemCount(task.id()),
|
||||
unresolvedItemCount, photoCounts, photos, mapper.pickExceptionReasons(task.id()));
|
||||
}
|
||||
|
||||
public PhotoDownload downloadPhoto(long containerBookingId, long fileId) {
|
||||
ContainerBookingEntity booking = required(containerBookingId);
|
||||
var task = mapper.pickTask(booking.getId());
|
||||
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "货柜尚无PDA作业任务");
|
||||
var photo = mapper.photos(task.loadingPlanId()).stream()
|
||||
.filter(value -> Long.valueOf(fileId).equals(value.fileId()) && "ACTIVE".equals(value.status()))
|
||||
.findFirst().orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "PDA照片不存在或不属于该货柜"));
|
||||
final org.springframework.http.ResponseEntity<byte[]> response;
|
||||
try {
|
||||
response = evidenceFiles.download(internalToken, photo.tenantId(), photo.inboundId(), fileId);
|
||||
} catch (FeignException.NotFound exception) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA照片文件不存在或归属不匹配");
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
byte[] content = response.getBody();
|
||||
if (!response.getStatusCode().is2xxSuccessful() || content == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "PDA照片下载失败");
|
||||
}
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
return new PhotoDownload(photo.fileName(), contentType == null ? MediaType.APPLICATION_OCTET_STREAM : contentType, content);
|
||||
}
|
||||
|
||||
public record PhotoDownload(String fileName, MediaType contentType, byte[] content) {}
|
||||
|
||||
private List<TimelineEntry> timeline(long id, Long taskId, List<ExceptionHistory> exceptionHistory) {
|
||||
var result = new ArrayList<TimelineEntry>();
|
||||
mapper.transport(id).forEach(value -> result.add(new TimelineEntry("TRANSPORT_UPDATE", value.getReason(),
|
||||
|
||||
@@ -144,14 +144,18 @@ public class FbaPricingService {
|
||||
AdjustFeeRequest request, String idempotencyKey) {
|
||||
InboundPricingEntity inbound = repository.findInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
if (!isCompletedInbound(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) {
|
||||
throw conflict("只能修改已完成入库且已发布的费用");
|
||||
if (!isCompletedInbound(inbound.getProcessStatus())) {
|
||||
throw conflict("只能在待分柜阶段修改初算或已发布费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null || !"SENT".equals(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的已发布费用");
|
||||
if (current == null || !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的初算或已发布费用");
|
||||
}
|
||||
String discriminator = current.getId() + ":" + request.subtotalAmount() + ":"
|
||||
if (!request.expectedFeeVersion().equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重试");
|
||||
}
|
||||
String discriminator = current.getId() + ":" + request.expectedFeeVersion() + ":"
|
||||
+ request.chargeableQuantity() + ":" + request.unitPrice() + ":"
|
||||
+ request.additionalAmount() + ":" + request.reason().trim();
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(tenantId, operatorId, inboundId,
|
||||
"FEE_ADJUST", idempotencyKey, hash("ADJUST", inboundId, discriminator));
|
||||
@@ -166,30 +170,33 @@ public class FbaPricingService {
|
||||
adjusted.setId(null);
|
||||
adjusted.setFeeNo("FBAF-" + inboundId + "-" + nextVersion);
|
||||
adjusted.setFeeVersion(nextVersion);
|
||||
adjusted.setSubtotalAmount(request.subtotalAmount().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
adjusted.setChargeableQuantity(request.chargeableQuantity().setScale(4, RoundingMode.HALF_UP));
|
||||
adjusted.setConvertedQuantity(adjusted.getChargeableQuantity());
|
||||
adjusted.setSubtotalAmount(adjusted.getChargeableQuantity().multiply(request.unitPrice())
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
adjusted.setAdditionalAmount(request.additionalAmount().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
adjusted.setTotalAmount(adjusted.getSubtotalAmount().add(adjusted.getAdditionalAmount()));
|
||||
adjusted.setFeeStatus("SENT");
|
||||
adjusted.setReviewStatus("NOT_REQUIRED");
|
||||
boolean published = "SENT".equals(current.getFeeStatus());
|
||||
adjusted.setFeeStatus(published ? "SENT" : "CALCULATED");
|
||||
adjusted.setReviewStatus(published ? "NOT_REQUIRED" : "NOT_SUBMITTED");
|
||||
adjusted.setCalculatedBy(operatorId);
|
||||
adjusted.setCalculatedAt(now);
|
||||
adjusted.setConfirmedBy(operatorId);
|
||||
adjusted.setConfirmedAt(now);
|
||||
adjusted.setSentBy(operatorId);
|
||||
adjusted.setSentAt(now);
|
||||
adjusted.setConfirmedBy(published ? operatorId : null);
|
||||
adjusted.setConfirmedAt(published ? now : null);
|
||||
adjusted.setSentBy(published ? operatorId : null);
|
||||
adjusted.setSentAt(published ? now : null);
|
||||
adjusted.setCreatedAt(now);
|
||||
adjusted.setUpdatedAt(now);
|
||||
if (repository.supersedeSent(tenantId, current.getId()) != 1) {
|
||||
if (repository.supersedeCurrent(tenantId, current.getId(), current.getFeeStatus()) != 1) {
|
||||
throw conflict("费用已被其他操作员修改,请刷新后重试");
|
||||
}
|
||||
repository.insertFee(adjusted);
|
||||
BigDecimal quantity = current.getChargeableQuantity();
|
||||
BigDecimal quantity = adjusted.getChargeableQuantity();
|
||||
InboundFeeItemEntity base = item(tenantId, adjusted, 1, "FBA_BASE", "FBA基础运费",
|
||||
adjusted.getSubtotalAmount(), now);
|
||||
base.setQuantity(quantity);
|
||||
base.setUnit(current.getChargeUnit());
|
||||
base.setUnitPrice(quantity.signum() == 0 ? BigDecimal.ZERO
|
||||
: adjusted.getSubtotalAmount().divide(quantity, 6, RoundingMode.HALF_UP));
|
||||
base.setUnitPrice(request.unitPrice().setScale(6, RoundingMode.HALF_UP));
|
||||
base.setAutomaticFlag(false);
|
||||
base.setRemark(request.reason().trim());
|
||||
repository.insertItem(base);
|
||||
@@ -200,9 +207,15 @@ public class FbaPricingService {
|
||||
extra.setRemark(request.reason().trim());
|
||||
repository.insertItem(extra);
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now);
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
tenantId, inboundId, adjusted.getId(), nextVersion, now);
|
||||
if (repository.markInboundAdjusted(tenantId, inboundId, adjusted.getFeeStatus(),
|
||||
adjusted.getChargeUnit(), quantity) != 1) {
|
||||
throw conflict("入库单状态已变化,费用调整未保存");
|
||||
}
|
||||
if (published) {
|
||||
feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now);
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
tenantId, inboundId, adjusted.getId(), nextVersion, now);
|
||||
}
|
||||
complete(token, adjusted.getId(), now);
|
||||
return response(tenantId, adjusted.getId(), inbound.getProcessStatus());
|
||||
}
|
||||
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 发送费用并完成入库的状态编排骨架。
|
||||
*
|
||||
* <p>本服务不计算费用、不创建托盘/库位,也不调用仓库 CRUD。测量完成阶段已确认托盘和库位占用;
|
||||
* 客服审核通过后的自动编排调用 {@link #beginSendFeeAndInbound} 进入 INBOUNDING,再调用
|
||||
* {@link #completeSendFeeAndInbound} 发布费用并完成入库。</p>
|
||||
*/
|
||||
@Service
|
||||
public class InboundCompletionOrchestrator {
|
||||
@Resource
|
||||
private InboundWorkflowRepository repository;
|
||||
@Resource
|
||||
private InboundIdempotencyService idempotency;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
@Resource
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
|
||||
/**
|
||||
* 任务5对接方法一:锁定发送动作。允许前置状态仅为 FEE_CONFIRMING + CONFIRMED。
|
||||
*/
|
||||
@Transactional
|
||||
public void beginSendFeeAndInbound(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, long feeId, int expectedVersion,
|
||||
String idempotencyKey) {
|
||||
String canonical = inboundId + "|" + feeId + "|" + expectedVersion;
|
||||
InboundIdempotencyService.Context context = idempotency.begin(
|
||||
tenantId, operatorId, inboundId, "SEND_FEE_BEGIN", idempotencyKey, canonical);
|
||||
if (context.replayed()) {
|
||||
return;
|
||||
}
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
if (!"FEE_CONFIRMING".equals(order.getProcessStatus()) || !"CONFIRMED".equals(order.getFeeStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用未确认或入库单不在待发送阶段");
|
||||
}
|
||||
boolean released = measurementGatePassed(order, tenantId, inboundId);
|
||||
InboundFeeEntity fee = repository.findConfirmedFee(tenantId, inboundId, feeId);
|
||||
if (fee == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "缺少已确认且不可变的费用快照");
|
||||
}
|
||||
if (Boolean.TRUE.equals(fee.getExceptionRelease()) != released) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用快照的异常放行标记与审核历史不一致");
|
||||
}
|
||||
if (repository.countPreparedAllocations(tenantId, inboundId) <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "尚未准备托盘和库位预占引用");
|
||||
}
|
||||
if (repository.beginInbound(tenantId, inboundId, operatorId, expectedVersion) != 1) {
|
||||
throw concurrentConflict();
|
||||
}
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"FEE_CONFIRMING", "INBOUNDING", "SEND_FEE_BEGIN", null, operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务5对接方法二:仓库确认占用成功后,原子锁定费用、完成入库并写 Outbox。
|
||||
*/
|
||||
@Transactional
|
||||
public void completeSendFeeAndInbound(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, long feeId, int expectedVersion,
|
||||
String idempotencyKey) {
|
||||
String canonical = inboundId + "|" + feeId + "|" + expectedVersion;
|
||||
InboundIdempotencyService.Context context = idempotency.begin(
|
||||
tenantId, operatorId, inboundId, "SEND_FEE_COMPLETE", idempotencyKey, canonical);
|
||||
if (context.replayed()) {
|
||||
return;
|
||||
}
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
if (!"INBOUNDING".equals(order.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单不在入库处理中");
|
||||
}
|
||||
if (repository.completeInbound(tenantId, inboundId, operatorId, expectedVersion) != 1) {
|
||||
throw concurrentConflict();
|
||||
}
|
||||
if (repository.markFeeSent(tenantId, inboundId, feeId, operatorId) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用快照已被并发发送或状态不正确");
|
||||
}
|
||||
var published = feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId,
|
||||
operatorName, LocalDateTime.now());
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"INBOUNDING", "WAIT_SPLIT", "SEND_FEE_COMPLETE", null, operatorId, operatorName));
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, tenantId,
|
||||
inboundId, feeId, published.getSourceFeeVersion(), LocalDateTime.now());
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
/** 任务5对接方法三:仅在仓库预占已释放后回退,不能回退已完成实物入库。 */
|
||||
@Transactional
|
||||
public void rollbackSendFeeAndInbound(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, int expectedVersion, String reason,
|
||||
String idempotencyKey) {
|
||||
if (!StringUtils.hasText(reason)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "入库回退必须记录失败原因");
|
||||
}
|
||||
String canonical = inboundId + "|" + expectedVersion + "|" + (reason == null ? "" : reason.trim());
|
||||
InboundIdempotencyService.Context context = idempotency.begin(
|
||||
tenantId, operatorId, inboundId, "SEND_FEE_ROLLBACK", idempotencyKey, canonical);
|
||||
if (context.replayed()) {
|
||||
return;
|
||||
}
|
||||
requireOrder(tenantId, inboundId);
|
||||
if (repository.rollbackInbound(tenantId, inboundId, operatorId, expectedVersion) != 1) {
|
||||
throw concurrentConflict();
|
||||
}
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"INBOUNDING", "FEE_CONFIRMING", "SEND_FEE_ROLLBACK", reason, operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
private boolean measurementGatePassed(InboundOrderEntity order, long tenantId, long inboundId) {
|
||||
if ("APPROVED".equals(order.getMeasureReviewStatus())) {
|
||||
return false;
|
||||
}
|
||||
if ("REJECTED".equals(order.getMeasureReviewStatus())
|
||||
&& repository.hasExceptionRelease(tenantId, inboundId)) {
|
||||
return true;
|
||||
}
|
||||
throw new BizException(ResultCode.CONFLICT, "测量未审核通过且无有效异常放行记录");
|
||||
}
|
||||
|
||||
private InboundOrderEntity requireOrder(long tenantId, long inboundId) {
|
||||
InboundOrderEntity order = repository.findOwned(tenantId, inboundId);
|
||||
if (order == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private static BizException concurrentConflict() {
|
||||
return new BizException(ResultCode.CONFLICT, "入库单已被并发更新,请刷新后重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import com.baiyue.firstleg.dto.MeasurementApprovalResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Best-effort initial pricing; pricing availability never gates inbound workflow. */
|
||||
@Service
|
||||
public class InitialInboundPricingService {
|
||||
private static final Logger log = LoggerFactory.getLogger(InitialInboundPricingService.class);
|
||||
@Resource
|
||||
private FbaPricingService pricing;
|
||||
|
||||
public MeasurementApprovalResponse tryCalculate(long tenantId, long operatorId, long inboundId,
|
||||
long measurementId, int reviewedOrderVersion) {
|
||||
String key = "initial-" + inboundId + "-" + measurementId + "-" + reviewedOrderVersion;
|
||||
try {
|
||||
FeeSnapshotResponse fee = pricing.calculate(tenantId, operatorId, inboundId, null, key);
|
||||
return new MeasurementApprovalResponse(inboundId, measurementId, "WAIT_SPLIT",
|
||||
fee.feeStatus(), true, null, fee);
|
||||
} catch (BizException exception) {
|
||||
log.info("入库初算未生成,主流程继续 tenantId={}, inboundId={}, reason={}",
|
||||
tenantId, inboundId, exception.getMessage());
|
||||
return pending(inboundId, measurementId, exception.getMessage());
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("入库初算异常,主流程继续 tenantId={}, inboundId={}", tenantId, inboundId, exception);
|
||||
return pending(inboundId, measurementId, "初算暂未生成,请人工维护费用");
|
||||
}
|
||||
}
|
||||
|
||||
private static MeasurementApprovalResponse pending(long inboundId, long measurementId, String message) {
|
||||
return new MeasurementApprovalResponse(inboundId, measurementId, "WAIT_SPLIT", "UNPRICED",
|
||||
false, message, null);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 测量审核应用服务。审核通过后进入内部计费编排,完成费用发布和入库后再进入 WAIT_SPLIT。
|
||||
* 测量审核应用服务。审核通过后直接进入 WAIT_SPLIT,费用初算和人工维护不阻塞主流程。
|
||||
*/
|
||||
@Service
|
||||
public class MeasurementReviewService {
|
||||
@@ -99,7 +99,7 @@ public class MeasurementReviewService {
|
||||
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result,
|
||||
request.reason(), false, null, false, operatorId, operatorName));
|
||||
repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING",
|
||||
approved ? "WAIT_PRICING" : "MEASURE_REJECTED",
|
||||
approved ? "WAIT_SPLIT" : "MEASURE_REJECTED",
|
||||
approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class MeasurementReviewService {
|
||||
}
|
||||
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), "REJECTED",
|
||||
null, true, request.reason(), true, operatorId, operatorName));
|
||||
repository.insertHistory(history(tenantId, inboundId, "MEASURE_REJECTED", "WAIT_PRICING",
|
||||
repository.insertHistory(history(tenantId, inboundId, "MEASURE_REJECTED", "WAIT_SPLIT",
|
||||
"MEASURE_EXCEPTION_RELEASE", request.reason(), operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baiyue.firstleg.domain.PricingTaxMode;
|
||||
import com.baiyue.firstleg.dto.PriceBookDtos.CurrentChannel;
|
||||
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest;
|
||||
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateResponse;
|
||||
import com.baiyue.firstleg.dto.PriceBookChannelValidation.Response;
|
||||
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
@@ -47,14 +48,22 @@ public class MerchantPriceBookService {
|
||||
estimate.amount(), estimate.currency(), DISCLAIMER);
|
||||
}
|
||||
|
||||
public void requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode) {
|
||||
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
requireMerchant(merchantTenantId);
|
||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||
Long versionId = repository.activeVersionId();
|
||||
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId,
|
||||
normalizeChannel(channelCode))) {
|
||||
String normalized = normalizeChannel(channelCode);
|
||||
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId, normalized)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道已下线,请重新选择");
|
||||
}
|
||||
var prefix = repository.channelPrefix(normalized);
|
||||
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
||||
}
|
||||
String selected = "OVERSEAS".equals(destinationType) && StringUtils.hasText(prefix.overseasInboundCodePrefix())
|
||||
? prefix.overseasInboundCodePrefix() : prefix.inboundCodePrefix();
|
||||
return new Response(normalized, selected);
|
||||
}
|
||||
|
||||
private CurrentChannel toChannel(ChannelRow row, Map<String, List<PricingTaxMode>> taxModes) {
|
||||
|
||||
@@ -35,20 +35,21 @@ public class TruckAppointmentService {
|
||||
@Resource TruckAppointmentFileClient files;
|
||||
@Resource ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token}") String internalToken;
|
||||
public PageResult<TruckAppointmentEntity> page(Query q){return repo.page(q);} public TruckAppointmentEntity detail(long id){return required(id);}
|
||||
public PageResult<TruckAppointmentEntity> page(Query q){return repo.page(q);} public TruckAppointmentEntity detail(long id){return enrich(required(id));}
|
||||
public java.util.List<TruckAppointmentHistoryEntity> history(long id){required(id);return repo.histories(id);} public java.util.List<TruckAppointmentAttachmentEntity> attachments(long id){required(id);return repo.attachments(id);}
|
||||
@Transactional public TruckAppointmentAttachmentEntity upload(long id,String type,MultipartFile file,long uid){TruckAppointmentEntity appointment=required(id);if(appointment.getCustomerId()==null||appointment.getCustomerId()<=0)throw bad("预约缺少客户归属,无法上传附件");try{var response=files.upload(internalToken,uid,appointment.getCustomerId(),"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),file);if(response==null||!response.successful()||response.data()==null)throw bad("附件上传失败");var m=response.data();TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(m.fileId());a.setAttachmentType(type);a.setFileName(m.fileName());a.setFileHash(m.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");}}
|
||||
@Transactional public TruckAppointmentAttachmentEntity attach(long id,AttachmentRequest r,long uid){TruckAppointmentEntity appointment=required(id); if(appointment.getCustomerId()==null||appointment.getCustomerId()<=0)throw bad("预约缺少客户归属,无法引用附件"); TruckAppointmentFileClient.Metadata metadata;try{var response=files.metadata(internalToken,appointment.getCustomerId(),"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),r.fileId());if(response==null||!response.successful()||response.data()==null)throw bad("附件不存在或归属不匹配");metadata=response.data();}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");} TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(metadata.fileId());a.setAttachmentType(r.attachmentType());a.setFileName(metadata.fileName());a.setFileHash(metadata.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}
|
||||
@Transactional public void deleteAttachment(long id,long attachmentId){required(id);if(repo.deleteAttachment(id,attachmentId)!=1)throw new BizException(ResultCode.NOT_FOUND,"附件引用不存在");}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0"); validateBookingLink(r); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo("TA"+e.getId()); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); validateBookingLink(r); copy(e,r); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0"); OceanBookingEntity booking=validateBookingLink(r); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo("TA"+e.getId()); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r,booking); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); OceanBookingEntity booking=validateBookingLink(r); copy(e,r,booking); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String clientHash,long uid,String name){String requestHash=hash(action,id,r);verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if("RESCHEDULE".equals(action))throw conflict("改约请使用专用接口"); TruckAppointmentStatus from=e.getStatus(),to=next(from,action); if(repo.transition(id,from.name(),to.name(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试"); TruckAppointmentEntity after=required(id);record(after,from.name(),to.name(),action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,String idem,String clientHash,long uid,String name){String action="RESCHEDULE";String requestHash=hash(action,id,new ReschedulePayload(at,r));verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");TruckAppointmentEntity after=required(id);record(after,from,"RESCHEDULED",action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
private void validateBookingLink(SaveRequest r){if(r.oceanBookingId()==null||r.oceanBookingId()<=0)throw bad("oceanBookingId不能为空"); OceanBookingEntity b=oceanBookings.find(r.oceanBookingId()); if(b==null)throw new BizException(ResultCode.NOT_FOUND,"关联订舱不存在"); if(!"RELEASED".equals(b.getStatus()))throw conflict("仅已放舱订舱可创建拖车预约"); if(r.customerId()!=null&&r.customerId()>0&&!Objects.equals(r.customerId(),b.getCustomerId()))throw bad("customerId与订舱客户不一致"); if(r.containerBookingId()!=null&&r.containerBookingId()>0){ContainerBookingEntity c=oceanBookings.containerBooking(r.containerBookingId()); if(c==null)throw new BizException(ResultCode.NOT_FOUND,"关联柜体不存在"); boolean allocated=oceanBookings.allocations(r.oceanBookingId()).stream().anyMatch(a->Objects.equals(a.getContainerBookingId(),r.containerBookingId())); if(!allocated)throw bad("containerBookingId未分配至该订舱"); if(r.warehouseId()!=null&&r.warehouseId()>0&&c.getWarehouseId()!=null&&!Objects.equals(r.warehouseId(),c.getWarehouseId()))throw bad("warehouseId与柜体不一致");} validateLoadingPlanLink(r);}
|
||||
private OceanBookingEntity validateBookingLink(SaveRequest r){if(r.oceanBookingId()==null||r.oceanBookingId()<=0)throw bad("oceanBookingId不能为空"); OceanBookingEntity b=oceanBookings.find(r.oceanBookingId()); if(b==null)throw new BizException(ResultCode.NOT_FOUND,"关联订舱不存在"); if(!"RELEASED".equals(b.getStatus()))throw conflict("仅已放舱订舱可创建拖车预约"); if(b.getCustomerId()==null||!StringUtils.hasText(b.getCustomerName()))throw conflict("关联订舱缺少客户快照"); if(r.customerId()!=null&&r.customerId()>0&&!Objects.equals(r.customerId(),b.getCustomerId()))throw bad("customerId与订舱客户不一致"); if(StringUtils.hasText(r.customerName())&&!r.customerName().trim().equals(b.getCustomerName()))throw bad("customerName与订舱客户快照不一致"); if(r.containerBookingId()!=null&&r.containerBookingId()>0){ContainerBookingEntity c=oceanBookings.containerBooking(r.containerBookingId()); if(c==null)throw new BizException(ResultCode.NOT_FOUND,"关联柜体不存在"); boolean allocated=oceanBookings.allocations(r.oceanBookingId()).stream().anyMatch(a->Objects.equals(a.getContainerBookingId(),r.containerBookingId())); if(!allocated)throw bad("containerBookingId未分配至该订舱"); if(r.warehouseId()!=null&&r.warehouseId()>0&&c.getWarehouseId()!=null&&!Objects.equals(r.warehouseId(),c.getWarehouseId()))throw bad("warehouseId与柜体不一致");} validateLoadingPlanLink(r);return b;}
|
||||
private void validateLoadingPlanLink(SaveRequest r){if(r.loadingPlanId()==null)return;if(r.loadingPlanId()<=0)throw bad("loadingPlanId必须大于0");if(r.containerBookingId()==null||r.containerBookingId()<=0)throw bad("关联装柜计划时containerBookingId不能为空");LoadingPlanEntity plan=loadingPlans.plan(r.loadingPlanId());if(plan==null)throw new BizException(ResultCode.NOT_FOUND,"关联装柜计划不存在");if(!Objects.equals(plan.getContainerBookingId(),r.containerBookingId()))throw bad("loadingPlanId与柜体不一致");}
|
||||
private TruckAppointmentStatus next(TruckAppointmentStatus s,String a){return switch(a){case"SUBMIT"->s==TruckAppointmentStatus.DRAFT?TruckAppointmentStatus.PENDING_DISPATCH:badStatus();case"DISPATCH"->s==TruckAppointmentStatus.PENDING_DISPATCH||s==TruckAppointmentStatus.RESCHEDULED?TruckAppointmentStatus.DISPATCHED:badStatus();case"PICKUP"->s==TruckAppointmentStatus.DISPATCHED?TruckAppointmentStatus.PICKUP_IN_PROGRESS:badStatus();case"ARRIVE"->s==TruckAppointmentStatus.PICKUP_IN_PROGRESS?TruckAppointmentStatus.ARRIVED_WAREHOUSE:badStatus();case"LOAD"->s==TruckAppointmentStatus.ARRIVED_WAREHOUSE?TruckAppointmentStatus.LOADING:badStatus();case"DEPART"->s==TruckAppointmentStatus.LOADING?TruckAppointmentStatus.DEPARTED:badStatus();case"COMPLETE"->s==TruckAppointmentStatus.DEPARTED?TruckAppointmentStatus.COMPLETED:badStatus();case"EXCEPTION"->s!=TruckAppointmentStatus.COMPLETED&&s!=TruckAppointmentStatus.CANCELLED?TruckAppointmentStatus.EXCEPTION:badStatus();case"CANCEL"->s!=TruckAppointmentStatus.COMPLETED?TruckAppointmentStatus.CANCELLED:badStatus();default->badStatus();};}
|
||||
private TruckAppointmentStatus badStatus(){throw conflict("不允许的状态动作");} private TruckAppointmentEntity required(long id){TruckAppointmentEntity e=repo.find(id);if(e==null)throw new BizException(ResultCode.NOT_FOUND,"拖车预约不存在");return e;} private void checkVersion(TruckAppointmentEntity e,int v){if(e.getVersion()!=v)throw conflict("版本已变化,请刷新后重试");} private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);} private static BizException bad(String m){return new BizException(ResultCode.BAD_REQUEST,m);}
|
||||
private void copy(TruckAppointmentEntity e,SaveRequest r){e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(r.containerBookingId());e.setLoadingPlanId(r.loadingPlanId());e.setWarehouseId(r.warehouseId());e.setCustomerId(r.customerId());e.setCustomerName(r.customerName());e.setCarrierId(r.carrierId());e.setCarrierName(r.carrierName());e.setFleetContact(r.fleetContact());e.setFleetPhone(r.fleetPhone());e.setDriverName(r.driverName());e.setDriverPhone(r.driverPhone());e.setDriverIdCard(r.driverIdCard());e.setVehicleNo(r.vehicleNo());e.setStoreName(r.storeName());e.setStoreContact(r.storeContact());e.setStorePhone(r.storePhone());e.setStoreAddress(r.storeAddress());e.setStoreCity(r.storeCity());e.setStoreZipcode(r.storeZipcode());e.setPackageCount(r.packageCount());e.setGrossWeightKg(r.grossWeightKg());e.setVolumeCbm(r.volumeCbm());e.setPalletCount(r.palletCount());e.setPoNo(r.poNo());e.setPlannedLoadingAt(r.plannedLoadingAt());e.setTransitMinutes(r.transitMinutes());e.setPayableAmount(r.payableAmount());e.setReceivableAmount(r.receivableAmount());e.setCurrency(r.currency()==null?"RMB":r.currency());e.setRemark(r.remark());}
|
||||
private TruckAppointmentEntity enrich(TruckAppointmentEntity value){OceanBookingEntity booking=oceanBookings.find(value.getOceanBookingId());if(booking!=null){value.setOceanBookingNo(booking.getBookingNo());value.setWarehouseName(booking.getWarehouseName());value.setShippingCompanyId(booking.getShippingCompanyId());value.setShippingCompanyName(booking.getShippingCompanyName());value.setBookingAgentId(booking.getBookingAgentId());value.setBookingAgentName(booking.getBookingAgentName());}if(value.getContainerBookingId()!=null){ContainerBookingEntity container=oceanBookings.containerBooking(value.getContainerBookingId());if(container!=null){value.setCabinetNo(container.getCabinetNo());value.setContainerNo(container.getContainerNo());if(!StringUtils.hasText(value.getWarehouseName()))value.setWarehouseName(container.getWarehouseName());}}return value;}
|
||||
private void copy(TruckAppointmentEntity e,SaveRequest r,OceanBookingEntity booking){e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(r.containerBookingId());e.setLoadingPlanId(r.loadingPlanId());e.setWarehouseId(r.warehouseId());e.setCustomerId(booking.getCustomerId());e.setCustomerName(booking.getCustomerName());e.setCarrierId(r.carrierId());e.setCarrierName(r.carrierName());e.setFleetContact(r.fleetContact());e.setFleetPhone(r.fleetPhone());e.setDriverName(r.driverName());e.setDriverPhone(r.driverPhone());e.setDriverIdCard(r.driverIdCard());e.setVehicleNo(r.vehicleNo());e.setStoreName(r.storeName());e.setStoreContact(r.storeContact());e.setStorePhone(r.storePhone());e.setStoreAddress(r.storeAddress());e.setStoreCity(r.storeCity());e.setStoreZipcode(r.storeZipcode());e.setPackageCount(r.packageCount());e.setGrossWeightKg(r.grossWeightKg());e.setVolumeCbm(r.volumeCbm());e.setPalletCount(r.palletCount());e.setPoNo(r.poNo());e.setPlannedLoadingAt(r.plannedLoadingAt());e.setTransitMinutes(r.transitMinutes());e.setPayableAmount(r.payableAmount());e.setReceivableAmount(r.receivableAmount());e.setCurrency(r.currency()==null?"RMB":r.currency());e.setRemark(r.remark());}
|
||||
private TruckAppointmentEntity claimOrReplay(long id,String action,String key,String hash){validateKey(key);String normalized=key.trim();if(repo.claimOperation(IdWorker.getId(),id,action,normalized,hash))return null;TruckAppointmentOperationEntity existing=repo.operation(normalized);if(existing==null||!Objects.equals(existing.getAppointmentId(),id)||!action.equals(existing.getAction())||!hash.equals(existing.getRequestHash()))throw conflict("Idempotency-Key已用于不同请求");if(!StringUtils.hasText(existing.getResponseSnapshot()))throw conflict("相同请求正在处理中,请稍后重试");try{return objectMapper.readValue(existing.getResponseSnapshot(),TruckAppointmentEntity.class);}catch(JsonProcessingException ex){throw new IllegalStateException("无法读取拖车预约幂等结果",ex);}}
|
||||
private TruckAppointmentEntity completeOperation(long id,String action,String key,TruckAppointmentEntity result){try{String normalized=key.trim();if(repo.completeOperation(id,action,normalized,objectMapper.writeValueAsString(result))!=1)throw conflict("拖车预约幂等请求完成状态冲突");return result;}catch(JsonProcessingException ex){throw new IllegalStateException("无法保存拖车预约幂等结果",ex);}}
|
||||
private String hash(String action,long id,Object request){try{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest((action+'|'+id+'|'+objectMapper.writeValueAsString(request)).getBytes(StandardCharsets.UTF_8)));}catch(Exception ex){throw new IllegalStateException("无法计算拖车预约请求摘要",ex);}}
|
||||
|
||||
Reference in New Issue
Block a user