优化
This commit is contained in:
+6
@@ -90,6 +90,12 @@ public class LoadingModelBridgeService {
|
||||
throw conflict("PDA开始前的预装关联不完整,禁止在LOADING后补关联");
|
||||
}
|
||||
for (AllocationScope scope : scopes) {
|
||||
// Internal push replay is pair-idempotent: an existing association has
|
||||
// already advanced both versions, so calling public bind again would
|
||||
// derive a different request hash for the same AUTO key.
|
||||
if (repository.byPair(scope.tenantId(), scope.inboundId(), loadingPlanId) != null) {
|
||||
continue;
|
||||
}
|
||||
var preload = repository.preload(scope.tenantId(), scope.inboundId());
|
||||
var currentPlan = repository.loadingPlan(loadingPlanId);
|
||||
if (preload == null || currentPlan == null) {
|
||||
|
||||
+11
@@ -18,6 +18,7 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** 平台端装柜证据和国内装柜完成操作。 */
|
||||
@@ -58,6 +59,16 @@ public class PlatformLoadingDeliveryController {
|
||||
return ApiResponse.success(service.evidence(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA查看装柜证据照片")
|
||||
@GetMapping("/{inboundId}/loading/evidence/{evidenceId}/content/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ResponseEntity<byte[]> pdaEvidenceContent(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long evidenceId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return service.evidenceContent(tenantId, inboundId, evidenceId);
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA作废错误装柜证据")
|
||||
@PostMapping("/{inboundId}/loading/evidence/{evidenceId}/void/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
|
||||
@@ -7,7 +7,7 @@ public final class LoadingResponses {
|
||||
private LoadingResponses() {}
|
||||
|
||||
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
|
||||
Long fileId, String fileName, String sealNo, String remark,
|
||||
Long fileId, String fileName, String previewUrl, String sealNo, String remark,
|
||||
Integer version, String status, String voidReason,
|
||||
Long voidedBy, String voidedByName, LocalDateTime voidedAt,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
+23
-1
@@ -24,6 +24,7 @@ import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -149,6 +150,22 @@ public class LoadingDeliveryService {
|
||||
return evidenceResponse(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> evidenceContent(long tenantId, long inboundId, long evidenceId) {
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
LoadingEvidenceEntity evidence = repository.listEvidence(tenantId, inboundId).stream()
|
||||
.filter(item -> Long.valueOf(evidenceId).equals(item.getId()) && isActive(item))
|
||||
.findFirst().orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "装柜证据不存在"));
|
||||
try {
|
||||
ResponseEntity<byte[]> response = files.download(internalToken, tenantId, inboundId, evidence.getFileId());
|
||||
if (response == null || response.getBody() == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "装柜证据文件不存在");
|
||||
}
|
||||
return response;
|
||||
} catch (FeignException ex) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse voidEvidence(long tenantId, long inboundId, long evidenceId, long operatorId,
|
||||
String operatorName, String key, VoidEvidenceRequest request) {
|
||||
@@ -333,11 +350,16 @@ public class LoadingDeliveryService {
|
||||
private EvidenceListResponse evidenceResponse(long tenantId, long inboundId) {
|
||||
List<EvidenceResponse> values = repository.listEvidence(tenantId, inboundId).stream().map(e ->
|
||||
new EvidenceResponse(e.getId(), e.getInboundId(), e.getTenantId(), e.getEvidenceType(), e.getFileId(),
|
||||
e.getFileName(), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
|
||||
e.getFileName(), evidencePreviewUrl(tenantId, inboundId, e.getId()), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
|
||||
e.getVoidedBy(), e.getVoidedByName(), e.getVoidedAt(), e.getCreatedAt(), e.getUpdatedAt())).toList();
|
||||
return new EvidenceListResponse(inboundId, values);
|
||||
}
|
||||
|
||||
private static String evidencePreviewUrl(long tenantId, long inboundId, long evidenceId) {
|
||||
return "/api/platform/v1/inbounds/" + inboundId + "/loading/evidence/" + evidenceId
|
||||
+ "/content/pda?tenantId=" + tenantId;
|
||||
}
|
||||
|
||||
private static boolean isActive(LoadingEvidenceEntity evidence) {
|
||||
return evidence != null && (evidence.getStatus() == null || "ACTIVE".equals(evidence.getStatus()));
|
||||
}
|
||||
|
||||
@@ -233,20 +233,18 @@ public class SmartLoadingService {
|
||||
public Plan pushPreload(long planId, String idempotencyKey, PushRequest request, long operatorId, String operatorName) {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
String requestHash = sha256("PUSH_PRELOAD|" + planId + '|' + request.expectedVersion());
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
if (!StringUtils.hasText(booking.getContainerNo())) {
|
||||
throw validation("录入真实柜号后才能推送预装清单并开启PDA作业");
|
||||
}
|
||||
if (key.equals(plan.getPushIdempotencyKey())) {
|
||||
if ("EDITING".equals(plan.getPlanStatus())) {
|
||||
ensurePendingPreloads(repository.items(planId), operatorId, operatorName);
|
||||
}
|
||||
loadingModelBridge.bindAllocatedBeforeLoading(plan.getId(), operatorId, operatorName, key);
|
||||
loadingPickService.beginIfReady(plan.getContainerBookingId(), operatorId, operatorName);
|
||||
requirePushReplay(planId, key, requestHash);
|
||||
return view(plan, booking);
|
||||
}
|
||||
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
|
||||
if (repository.operation(key) != null) throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
ensurePendingPreloads(items, operatorId, operatorName);
|
||||
@@ -254,13 +252,26 @@ public class SmartLoadingService {
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash)), operatorId, operatorName);
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
loadingModelBridge.bindAllocatedBeforeLoading(planId, operatorId, operatorName, key);
|
||||
loadingPickService.beginIfReady(pushed.getContainerBookingId(), operatorId, operatorName);
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private void requirePushReplay(long planId, String key, String requestHash) {
|
||||
LoadingPlanHistoryEntity history = repository.operation(key);
|
||||
if (history == null
|
||||
|| !Objects.equals(history.getLoadingPlanId(), planId)
|
||||
|| !Objects.equals(history.getActionType(), "PUSH_PRELOAD")
|
||||
|| history.getAfterSnapshot() == null
|
||||
|| !sameRequestHash(history.getAfterSnapshot(), requestHash)) {
|
||||
throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePendingPreloads(List<LoadingPlanItemEntity> items, long operatorId, String operatorName) {
|
||||
if (items.isEmpty()) throw validation("装柜明细不能为空");
|
||||
if (items.stream().anyMatch(item -> item.getAccountId() == null || item.getInboundId() == null)) {
|
||||
|
||||
Reference in New Issue
Block a user