fix(firstleg): scope loading evidence to cabinet tasks
This commit is contained in:
@@ -6,20 +6,35 @@ 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;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** 仅用于确认证据文件属于当前客户和入库单,不在 firstleg 复制文件元数据。 */
|
||||
/** Validate that a loading evidence file is owned by the physical cabinet. */
|
||||
@FeignClient(name = "file-service", contextId = "firstlegLoadingEvidenceFileClient",
|
||||
url = "${baiyue.clients.file.url:}", path = "/internal/file/v1/loading-evidence")
|
||||
public interface LoadingEvidenceFileClient {
|
||||
@GetMapping("/{tenantId}/{inboundId}/{fileId}")
|
||||
@PostMapping(value = "/{containerBookingId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
ApiResponse<UploadResult> upload(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId, @PathVariable long containerBookingId,
|
||||
@RequestParam String evidenceType, @RequestPart("file") MultipartFile file);
|
||||
|
||||
@GetMapping("/{containerBookingId}/{fileId}")
|
||||
ApiResponse<FileMetadata> validate(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long containerBookingId,
|
||||
@RequestParam String evidenceType,
|
||||
@PathVariable long fileId);
|
||||
|
||||
@GetMapping("/{tenantId}/{inboundId}/{fileId}/content")
|
||||
@GetMapping("/{containerBookingId}/{fileId}/content")
|
||||
ResponseEntity<byte[]> download(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long containerBookingId,
|
||||
@RequestParam String evidenceType,
|
||||
@PathVariable long fileId);
|
||||
|
||||
record FileMetadata(Long fileId, String fileName, String businessType, String contentType) {}
|
||||
record FileMetadata(Long fileId, String ownerScope, Long tenantId, String businessType,
|
||||
String businessId, String fileName, String contentType) {}
|
||||
record UploadResult(Long fileId, String ownerScope, Long tenantId, String businessType,
|
||||
String businessId, String fileName, String contentType, Long sizeBytes) {}
|
||||
}
|
||||
|
||||
+53
@@ -13,10 +13,17 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.Operation;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.EvidenceList;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.VoidRequest;
|
||||
import java.util.List;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.baiyue.firstleg.loading.service.CabinetLoadingEvidenceService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@@ -41,6 +48,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class PlatformLoadingPdaController {
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
@Resource
|
||||
private CabinetLoadingEvidenceService evidenceService;
|
||||
|
||||
@GetMapping("/tasks")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
@@ -70,6 +79,50 @@ public class PlatformLoadingPdaController {
|
||||
return ApiResponse.success(service.task(taskId, warehouseId));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/tasks/{taskId}/evidence", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<EvidenceList> upsertEvidence(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@RequestHeader("Idempotency-Key") @NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@RequestParam @Positive long warehouseId,
|
||||
@RequestParam @PositiveOrZero int expectedVersion,
|
||||
@RequestParam @NotBlank @Size(max = 32) String evidenceType,
|
||||
@RequestParam(required = false) @Size(max = 64) String sealNo,
|
||||
@RequestParam(required = false) @Size(max = 1000) String remark,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(evidenceService.upload(taskId, warehouseId, idempotencyKey,
|
||||
expectedVersion, evidenceType, sealNo, remark, file, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/evidence")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<EvidenceList> evidence(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(evidenceService.list(taskId, warehouseId, user.userId()));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/evidence/{evidenceId}/content")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ResponseEntity<byte[]> evidenceContent(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId, @PathVariable @Positive long evidenceId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return evidenceService.content(taskId, warehouseId, evidenceId, user.userId());
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/evidence/{evidenceId}/void")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<EvidenceList> voidEvidence(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId, @PathVariable @Positive long evidenceId,
|
||||
@Valid @RequestBody VoidRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(evidenceService.voidEvidence(taskId, evidenceId, request,
|
||||
user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/inventory-snapshot")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<InventorySnapshot> inventorySnapshot(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** PDA task-scoped cabinet evidence contract. */
|
||||
public final class LoadingEvidenceDtos {
|
||||
private LoadingEvidenceDtos() {}
|
||||
|
||||
public record UpsertRequest(@NotNull @Positive Long warehouseId,
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 32) String evidenceType,
|
||||
@NotNull @Positive Long fileId,
|
||||
@Size(max = 64) String sealNo,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record VoidRequest(@NotNull @Positive Long warehouseId,
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 500) String reason) {}
|
||||
|
||||
public record Evidence(String evidenceId, String evidenceType, String fileId, String fileName,
|
||||
String sealNo, String remark, String status, Integer version,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record EvidenceList(String taskId, String containerBookingId, String cabinetNo,
|
||||
String containerNo, List<Evidence> evidences) {
|
||||
public EvidenceList { evidences = evidences == null ? List.of() : List.copyOf(evidences); }
|
||||
}
|
||||
}
|
||||
-77
@@ -4,10 +4,7 @@ import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.VoidEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.EvidenceListResponse;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.LoadingActionResponse;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.PickingResponse;
|
||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||
@@ -18,7 +15,6 @@ 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.*;
|
||||
|
||||
/** 平台端装柜证据和国内装柜完成操作。 */
|
||||
@@ -28,79 +24,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
public class PlatformLoadingDeliveryController {
|
||||
@Resource private LoadingDeliveryService service;
|
||||
|
||||
@Operation(summary = "新增或修改装柜证据")
|
||||
@PostMapping("/{inboundId}/loading/evidence")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:edit')")
|
||||
@BusinessAudit(event = "INBOUND_LOADING_EVIDENCE_UPSERT", resourceType = "INBOUND", resourceId = "#inboundId", description = "维护装柜证据")
|
||||
public ApiResponse<LoadingActionResponse> evidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpsertEvidenceRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.upsertEvidence(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
/** PDA现场拍照专用入口。与后台人工上传使用不同权限,避免拣货员获得后台证据维护入口。 */
|
||||
@Operation(summary = "PDA追加装柜证据")
|
||||
@PostMapping("/{inboundId}/loading/evidence/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<LoadingActionResponse> pdaEvidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpsertEvidenceRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.upsertEvidence(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA查询装柜证据")
|
||||
@GetMapping("/{inboundId}/loading/evidence/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<EvidenceListResponse> pdaEvidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
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')")
|
||||
public ApiResponse<LoadingActionResponse> pdaVoidEvidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long evidenceId, @RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody VoidEvidenceRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.voidEvidence(tenantId, inboundId, evidenceId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询装柜证据")
|
||||
@GetMapping("/{inboundId}/loading/evidence")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:view')")
|
||||
public ApiResponse<EvidenceListResponse> evidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.evidence(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "作废装柜证据并保留审计记录")
|
||||
@PostMapping("/{inboundId}/loading/evidence/{evidenceId}/void")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:edit')")
|
||||
@BusinessAudit(event = "INBOUND_LOADING_EVIDENCE_VOID", resourceType = "INBOUND", resourceId = "#inboundId", description = "作废装柜证据")
|
||||
public ApiResponse<LoadingActionResponse> voidEvidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long evidenceId, @RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody VoidEvidenceRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.voidEvidence(tenantId, inboundId, evidenceId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成装柜")
|
||||
@PostMapping("/{inboundId}/loading/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:complete')")
|
||||
|
||||
@@ -9,18 +9,6 @@ import jakarta.validation.constraints.Size;
|
||||
public final class LoadingRequests {
|
||||
private LoadingRequests() {}
|
||||
|
||||
public record UpsertEvidenceRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 32) String evidenceType,
|
||||
@NotNull @Positive Long fileId,
|
||||
@Size(max = 255) String fileName,
|
||||
@Size(max = 64) String sealNo,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record VoidEvidenceRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 500) String reason) {}
|
||||
|
||||
public record CompleteLoadingRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package com.baiyue.firstleg.loading.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class LoadingResponses {
|
||||
private LoadingResponses() {}
|
||||
|
||||
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
|
||||
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) {}
|
||||
|
||||
public record EvidenceListResponse(Long inboundId, List<EvidenceResponse> evidences) {}
|
||||
|
||||
public record LoadingActionResponse(String mainStatus, EvidenceListResponse evidences) {}
|
||||
public record LoadingActionResponse(String mainStatus) {}
|
||||
|
||||
public record PickingResponse(Long id, Long inboundId, String status, Integer version,
|
||||
Integer expectedCount, Integer pickedCount, String remark) {}
|
||||
|
||||
+7
-5
@@ -10,19 +10,21 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_evidence")
|
||||
public class LoadingEvidenceEntity {
|
||||
@TableName("fl_loading_cabinet_evidence")
|
||||
public class CabinetLoadingEvidenceEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long containerBookingId;
|
||||
private Long sourceTaskId;
|
||||
private String evidenceType;
|
||||
private Long fileId;
|
||||
private String fileName;
|
||||
private String sealNo;
|
||||
private String remark;
|
||||
/** ACTIVE records count towards the loading gate; VOID records remain for audit only. */
|
||||
private String status;
|
||||
private Integer activeFlag;
|
||||
private String operationKey;
|
||||
private String requestHash;
|
||||
private String voidReason;
|
||||
private Long voidedBy;
|
||||
private String voidedByName;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CabinetLoadingEvidenceMapper extends BaseMapper<CabinetLoadingEvidenceEntity> {
|
||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} ORDER BY evidence_type,id")
|
||||
List<CabinetLoadingEvidenceEntity> byContainer(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} AND evidence_type=#{type} AND active_flag=1 LIMIT 1 FOR UPDATE")
|
||||
CabinetLoadingEvidenceEntity activeForUpdate(@Param("containerBookingId") long containerBookingId,
|
||||
@Param("type") String type);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE source_task_id=#{taskId} AND operation_key=#{key} LIMIT 1")
|
||||
CabinetLoadingEvidenceEntity byOperation(@Param("taskId") long taskId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE evidence_type='SEAL' AND seal_no=#{sealNo} AND active_flag=1 LIMIT 1")
|
||||
CabinetLoadingEvidenceEntity activeSeal(@Param("sealNo") String sealNo);
|
||||
|
||||
@Select("""
|
||||
SELECT plan.container_booking_id
|
||||
FROM fl_loading_plan_item item
|
||||
JOIN fl_loading_plan plan ON plan.id=item.loading_plan_id
|
||||
WHERE item.account_id=#{tenantId} AND item.inbound_id=#{inboundId} AND item.deleted=0
|
||||
AND plan.plan_status IN ('EDITING','LOADING','COMPLETED')
|
||||
ORDER BY CASE plan.plan_status WHEN 'LOADING' THEN 0 WHEN 'EDITING' THEN 1 ELSE 2 END,item.id DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
Long containerBookingId(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Select("""
|
||||
SELECT evidence.* FROM fl_loading_cabinet_evidence evidence
|
||||
JOIN fl_loading_plan plan ON plan.container_booking_id=evidence.container_booking_id
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=plan.id AND item.deleted=0
|
||||
WHERE item.account_id=#{tenantId} AND item.inbound_id=#{inboundId}
|
||||
ORDER BY evidence.evidence_type,evidence.id
|
||||
""")
|
||||
List<CabinetLoadingEvidenceEntity> byInbound(@Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId);
|
||||
|
||||
@Update("UPDATE fl_loading_cabinet_evidence SET status='VOID',active_flag=NULL,void_reason=#{reason},voided_by=#{operatorId},voided_by_name=#{operatorName},voided_at=CURRENT_TIMESTAMP(3),updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{id} AND container_booking_id=#{containerBookingId} AND active_flag=1 AND version=#{version}")
|
||||
int voidActive(@Param("id") long id, @Param("containerBookingId") long containerBookingId,
|
||||
@Param("version") int version, @Param("reason") String reason,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingEvidenceMapper extends BaseMapper<LoadingEvidenceEntity> {
|
||||
@Update("UPDATE fl_loading_evidence SET status='VOID', void_reason=#{reason}, voided_by=#{operatorId}, "
|
||||
+ "voided_by_name=#{operatorName}, voided_at=CURRENT_TIMESTAMP(3), updated_by=#{operatorId}, "
|
||||
+ "updated_by_name=#{operatorName}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND id=#{evidenceId} "
|
||||
+ "AND status='ACTIVE' AND version=#{expectedVersion}")
|
||||
int voidEvidence(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("evidenceId") long evidenceId, @Param("reason") String reason,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
}
|
||||
@@ -2,9 +2,7 @@ package com.baiyue.firstleg.loading.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingOperationEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingOperationMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
|
||||
@@ -17,7 +15,6 @@ import java.util.List;
|
||||
@Repository
|
||||
public class LoadingRepository {
|
||||
@Resource private LoadingInboundMapper inbounds;
|
||||
@Resource private LoadingEvidenceMapper evidences;
|
||||
@Resource private LoadingOperationMapper operations;
|
||||
@Resource private InboundLoadingPickTaskMapper picks;
|
||||
|
||||
@@ -46,39 +43,6 @@ public class LoadingRepository {
|
||||
return inbounds.aggregateRootLoaded(tenantId, rootInboundId, operatorId);
|
||||
}
|
||||
|
||||
public List<LoadingEvidenceEntity> listEvidence(long tenantId, long inboundId) {
|
||||
return evidences.selectList(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.orderByAsc(LoadingEvidenceEntity::getEvidenceType));
|
||||
}
|
||||
|
||||
public LoadingEvidenceEntity findEvidenceBySealNo(long tenantId, String sealNo) {
|
||||
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getEvidenceType, "SEAL")
|
||||
.eq(LoadingEvidenceEntity::getStatus, "ACTIVE")
|
||||
.eq(LoadingEvidenceEntity::getSealNo, sealNo));
|
||||
}
|
||||
|
||||
public LoadingEvidenceEntity findActiveEvidenceByFile(long tenantId, long inboundId,
|
||||
String evidenceType, long fileId) {
|
||||
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.eq(LoadingEvidenceEntity::getEvidenceType, evidenceType)
|
||||
.eq(LoadingEvidenceEntity::getFileId, fileId)
|
||||
.eq(LoadingEvidenceEntity::getStatus, "ACTIVE")
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
public void createEvidence(LoadingEvidenceEntity entity) { evidences.insert(entity); }
|
||||
|
||||
public int voidEvidence(long tenantId, long inboundId, long evidenceId, String reason,
|
||||
long operatorId, String operatorName, int expectedVersion) {
|
||||
return evidences.voidEvidence(tenantId, inboundId, evidenceId, reason, operatorId, operatorName, expectedVersion);
|
||||
}
|
||||
|
||||
public LoadingOperationEntity findOperation(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key) {
|
||||
return operations.selectOne(new LambdaQueryWrapper<LoadingOperationEntity>()
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.LoadingEvidenceFileClient;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.Evidence;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.EvidenceList;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.UpsertRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingEvidenceDtos.VoidRequest;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.loading.domain.LoadingEvidenceType;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.CabinetLoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Cabinet-owned evidence; taskId is only the audited command scope. */
|
||||
@Service
|
||||
public class CabinetLoadingEvidenceService {
|
||||
@Resource private CabinetLoadingEvidenceMapper mapper;
|
||||
@Resource private LoadingPickRepository loadingPicks;
|
||||
@Resource private LoadingEvidenceFileClient files;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
@Transactional
|
||||
public EvidenceList upsert(long taskId, String key, UpsertRequest request, long operatorId, String operatorName) {
|
||||
Scope scope = scope(taskId, request.warehouseId(), operatorId, true);
|
||||
requireKey(key);
|
||||
String requestHash = hash(request);
|
||||
CabinetLoadingEvidenceEntity replay = mapper.byOperation(taskId, key);
|
||||
if (replay != null) {
|
||||
if (!Objects.equals(requestHash, replay.getRequestHash())) throw conflict("相同幂等键的请求内容不一致");
|
||||
return view(scope);
|
||||
}
|
||||
String type = request.evidenceType().trim();
|
||||
if (!LoadingEvidenceType.VALUES.contains(type)) throw bad("不支持的装柜证据类型");
|
||||
String sealNo = LoadingEvidenceType.SEAL.equals(type) ? normalizedSeal(request.sealNo()) : null;
|
||||
CabinetLoadingEvidenceEntity duplicateSeal = sealNo == null ? null : mapper.activeSeal(sealNo);
|
||||
if (duplicateSeal != null && !Objects.equals(duplicateSeal.getContainerBookingId(), scope.containerBookingId())) {
|
||||
throw conflict("锁码已用于其他柜次");
|
||||
}
|
||||
var file = validateFile(scope.containerBookingId(), request.fileId(), type);
|
||||
CabinetLoadingEvidenceEntity current = mapper.activeForUpdate(scope.containerBookingId(), type);
|
||||
int expected = request.expectedVersion();
|
||||
if (current == null && expected != 0) throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||
if (current != null && !Objects.equals(current.getVersion(), expected)) throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||
if (current != null && Objects.equals(current.getFileId(), request.fileId())
|
||||
&& Objects.equals(current.getSealNo(), sealNo)) return view(scope);
|
||||
if (current != null && mapper.voidActive(current.getId(), scope.containerBookingId(), current.getVersion(),
|
||||
"REPLACED", operatorId, operatorName) != 1) throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
||||
value.setContainerBookingId(scope.containerBookingId()); value.setSourceTaskId(taskId);
|
||||
value.setEvidenceType(type); value.setFileId(request.fileId()); value.setFileName(file.fileName());
|
||||
value.setSealNo(sealNo); value.setRemark(request.remark()); value.setStatus("ACTIVE"); value.setActiveFlag(1);
|
||||
value.setOperationKey(key); value.setRequestHash(requestHash); value.setVersion(0);
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
||||
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
||||
CabinetLoadingEvidenceEntity concurrent = mapper.byOperation(taskId, key);
|
||||
if (concurrent != null && Objects.equals(concurrent.getRequestHash(), requestHash)) return view(scope);
|
||||
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
||||
}
|
||||
return view(scope);
|
||||
}
|
||||
|
||||
public EvidenceList upload(long taskId, long warehouseId, String key, int expectedVersion,
|
||||
String evidenceType, String sealNo, String remark, MultipartFile multipart,
|
||||
long operatorId, String operatorName) {
|
||||
Scope scope = scope(taskId, warehouseId, operatorId, true);
|
||||
try {
|
||||
var response = files.upload(internalToken, operatorId, scope.containerBookingId(), evidenceType, multipart);
|
||||
var uploaded = response == null ? null : response.data();
|
||||
if (uploaded == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "装柜证据文件上传失败");
|
||||
return upsert(taskId, key, new UpsertRequest(warehouseId, expectedVersion, evidenceType,
|
||||
uploaded.fileId(), sealNo, remark), operatorId, operatorName);
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
public EvidenceList list(long taskId, long warehouseId, long operatorId) {
|
||||
return view(scope(taskId, warehouseId, operatorId, false));
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> content(long taskId, long warehouseId, long evidenceId, long operatorId) {
|
||||
Scope scope = scope(taskId, warehouseId, operatorId, false);
|
||||
CabinetLoadingEvidenceEntity value = mapper.byContainer(scope.containerBookingId()).stream()
|
||||
.filter(item -> Objects.equals(item.getId(), evidenceId) && "ACTIVE".equals(item.getStatus()))
|
||||
.findFirst().orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "装柜证据不存在"));
|
||||
try { return files.download(internalToken, scope.containerBookingId(), value.getEvidenceType(), value.getFileId()); }
|
||||
catch (FeignException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用"); }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EvidenceList voidEvidence(long taskId, long evidenceId, VoidRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
Scope scope = scope(taskId, request.warehouseId(), operatorId, true);
|
||||
CabinetLoadingEvidenceEntity value = mapper.byContainer(scope.containerBookingId()).stream()
|
||||
.filter(item -> Objects.equals(item.getId(), evidenceId)).findFirst()
|
||||
.orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "装柜证据不存在"));
|
||||
if (!"ACTIVE".equals(value.getStatus())) throw conflict("装柜证据已经作废");
|
||||
if (!Objects.equals(value.getVersion(), request.expectedVersion()) || mapper.voidActive(value.getId(),
|
||||
scope.containerBookingId(), request.expectedVersion(), request.reason().trim(), operatorId, operatorName) != 1) {
|
||||
throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||
}
|
||||
return view(scope);
|
||||
}
|
||||
|
||||
public void requireComplete(long containerBookingId) {
|
||||
Set<String> active = mapper.byContainer(containerBookingId).stream()
|
||||
.filter(value -> "ACTIVE".equals(value.getStatus()))
|
||||
.map(CabinetLoadingEvidenceEntity::getEvidenceType).collect(Collectors.toSet());
|
||||
if (!active.containsAll(LoadingEvidenceType.VALUES)) throw conflict("柜次装柜证据未完成,请上传四步照片和带锁码照");
|
||||
}
|
||||
|
||||
public long requireContainerBookingId(long tenantId, long inboundId) {
|
||||
Long value = mapper.containerBookingId(tenantId, inboundId);
|
||||
if (value == null) throw conflict("入仓单未关联有效柜次,不能完成装柜");
|
||||
return value;
|
||||
}
|
||||
|
||||
private EvidenceList view(Scope scope) {
|
||||
List<Evidence> values = mapper.byContainer(scope.containerBookingId()).stream().map(value -> new Evidence(
|
||||
id(value.getId()), value.getEvidenceType(), id(value.getFileId()), value.getFileName(), value.getSealNo(),
|
||||
value.getRemark(), value.getStatus(), value.getVersion(), value.getCreatedAt(), value.getUpdatedAt())).toList();
|
||||
return new EvidenceList(id(scope.task().getId()), id(scope.containerBookingId()), scope.task().getCabinetNo(),
|
||||
scope.task().getContainerNo(), values);
|
||||
}
|
||||
|
||||
private Scope scope(long taskId, long warehouseId, long operatorId, boolean mutable) {
|
||||
LoadingPickTaskEntity task = loadingPicks.task(taskId);
|
||||
var booking = task == null ? null : loadingPicks.activeBookingByTask(taskId);
|
||||
if (task == null || task.getContainerBookingId() == null || booking == null
|
||||
|| !Objects.equals(task.getContainerBookingId(), booking.getId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA装柜任务不存在");
|
||||
}
|
||||
if (!Objects.equals(booking.getWarehouseId(), warehouseId)) throw new BizException(ResultCode.FORBIDDEN, "无权访问其他仓库的PDA任务");
|
||||
var assignment = loadingPicks.assignment(taskId);
|
||||
if (assignment == null || !Objects.equals(assignment.getAssigneeId(), operatorId)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "PDA任务未由当前操作人领取");
|
||||
}
|
||||
if (mutable && !"LOADING".equals(task.getTaskStatus())) throw conflict("PDA任务不在装柜作业中,证据不可修改");
|
||||
return new Scope(task, task.getContainerBookingId());
|
||||
}
|
||||
|
||||
private LoadingEvidenceFileClient.FileMetadata validateFile(long containerBookingId, long fileId, String type) {
|
||||
try {
|
||||
var response = files.validate(internalToken, containerBookingId, type, fileId);
|
||||
var value = response == null ? null : response.data();
|
||||
if (value == null || !"PLATFORM".equals(value.ownerScope()) || !Objects.equals(value.tenantId(), 0L)
|
||||
|| !Objects.equals(value.businessId(), Long.toString(containerBookingId))
|
||||
|| !Objects.equals(value.businessType(), "LOADING_" + type)) throw bad("装柜证据文件归属或类型不匹配");
|
||||
return value;
|
||||
} catch (FeignException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用"); }
|
||||
}
|
||||
|
||||
private String hash(Object value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(
|
||||
objectMapper.writeValueAsString(value).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (JsonProcessingException | NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
private static String normalizedSeal(String value) {
|
||||
if (!StringUtils.hasText(value)) throw bad("铅封证据必须填写锁码");
|
||||
return value.trim();
|
||||
}
|
||||
private static void requireKey(String key) { if (!StringUtils.hasText(key) || key.length() > 128) throw bad("Idempotency-Key不能为空且长度不能超过128"); }
|
||||
private static BizException bad(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
private static String id(Long value) { return value == null ? null : value.toString(); }
|
||||
private record Scope(LoadingPickTaskEntity task, long containerBookingId) {}
|
||||
}
|
||||
+17
-210
@@ -2,13 +2,10 @@ package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.LoadingEvidenceFileClient;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.loading.domain.LoadingEvidenceType;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.*;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.*;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingOperationEntity;
|
||||
import com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.loading.domain.PickingStatus;
|
||||
@@ -20,11 +17,7 @@ import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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;
|
||||
@@ -33,17 +26,12 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 装柜证据、拣货复核和国内装柜完成。 */
|
||||
@Service
|
||||
public class LoadingDeliveryService {
|
||||
private static final String UPSERT_EVIDENCE = "UPSERT_LOADING_EVIDENCE";
|
||||
private static final String VOID_EVIDENCE = "VOID_LOADING_EVIDENCE";
|
||||
private static final String COMPLETE_LOADING = "COMPLETE_LOADING";
|
||||
|
||||
@Resource private LoadingRepository repository;
|
||||
@@ -51,79 +39,16 @@ public class LoadingDeliveryService {
|
||||
@Resource private LoadingModelBridgeService loadingModelBridge;
|
||||
@Resource private LoadingCompletionGateService completionGate;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private LoadingEvidenceFileClient files;
|
||||
@Resource private CabinetLoadingEvidenceService cabinetEvidenceService;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse upsertEvidence(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, UpsertEvidenceRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, hash);
|
||||
if (replay != null) return replay;
|
||||
requireMutableLoadingStage(tenantId, inboundId);
|
||||
if (!LoadingEvidenceType.VALUES.contains(request.evidenceType())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不支持的装柜证据类型");
|
||||
}
|
||||
if (request.fileId() == null) throw new BizException(ResultCode.BAD_REQUEST, "装柜证据必须先上传真实文件");
|
||||
if (LoadingEvidenceType.SEAL.equals(request.evidenceType()) && !StringUtils.hasText(request.sealNo())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "铅封证据必须填写锁码");
|
||||
}
|
||||
if (LoadingEvidenceType.SEAL.equals(request.evidenceType())) {
|
||||
LoadingEvidenceEntity duplicate = repository.findEvidenceBySealNo(tenantId, request.sealNo().trim());
|
||||
if (duplicate != null && !Long.valueOf(inboundId).equals(duplicate.getInboundId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "锁码已用于其他装柜任务");
|
||||
}
|
||||
}
|
||||
LoadingEvidenceEntity existingFile = repository.findActiveEvidenceByFile(
|
||||
tenantId, inboundId, request.evidenceType(), request.fileId());
|
||||
if (existingFile != null) {
|
||||
String requestedSealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType())
|
||||
? request.sealNo().trim() : null;
|
||||
if (!Objects.equals(existingFile.getSealNo(), requestedSealNo)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "该带锁码照已绑定其他锁码,请先作废后重新拍摄");
|
||||
}
|
||||
LoadingActionResponse result = new LoadingActionResponse(
|
||||
requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, result);
|
||||
}
|
||||
var file = validateFile(tenantId, inboundId, request.fileId(), request.evidenceType());
|
||||
String fileName = file.fileName();
|
||||
String sealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType()) ? request.sealNo().trim() : null;
|
||||
try {
|
||||
// Evidence is append-only by type. A new photo never replaces an existing
|
||||
// one; an incorrect photo is explicitly voided through the void endpoint.
|
||||
LoadingEvidenceEntity entity = new LoadingEvidenceEntity();
|
||||
entity.setTenantId(tenantId); entity.setInboundId(inboundId);
|
||||
entity.setEvidenceType(request.evidenceType()); entity.setFileId(request.fileId());
|
||||
entity.setFileName(fileName); entity.setSealNo(sealNo); entity.setRemark(request.remark());
|
||||
entity.setStatus("ACTIVE"); entity.setVersion(0); entity.setCreatedBy(operatorId); entity.setCreatedByName(operatorName);
|
||||
entity.setUpdatedBy(operatorId); entity.setUpdatedByName(operatorName);
|
||||
repository.createEvidence(entity);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
throw new BizException(ResultCode.CONFLICT, "锁码已用于其他装柜任务");
|
||||
}
|
||||
LoadingActionResponse result = new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, result);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse completeLoading(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, CompleteLoadingRequest request) {
|
||||
return completeLoading(tenantId, inboundId, tenantId, inboundId,
|
||||
operatorId, operatorName, key, request);
|
||||
return completeLoading(tenantId, inboundId, operatorId, operatorName, key, request, false);
|
||||
}
|
||||
|
||||
private LoadingActionResponse completeLoading(long tenantId, long inboundId,
|
||||
long evidenceTenantId, long evidenceInboundId,
|
||||
long operatorId, String operatorName, String key,
|
||||
CompleteLoadingRequest request) {
|
||||
return completeLoading(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
|
||||
operatorId, operatorName, key, request, false);
|
||||
}
|
||||
|
||||
private LoadingActionResponse completeLoading(long tenantId, long inboundId,
|
||||
long evidenceTenantId, long evidenceInboundId,
|
||||
long operatorId, String operatorName, String key,
|
||||
CompleteLoadingRequest request,
|
||||
boolean acceptedPdaCompletion) {
|
||||
@@ -134,23 +59,23 @@ public class LoadingDeliveryService {
|
||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
||||
if (target != null) {
|
||||
return completeFulfillmentSlice(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
|
||||
operatorId, operatorName, key, request, target, acceptedPdaCompletion);
|
||||
return completeFulfillmentSlice(tenantId, inboundId, operatorId, operatorName, key,
|
||||
request, target, acceptedPdaCompletion);
|
||||
}
|
||||
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
||||
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse(inbound.processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
new LoadingActionResponse(inbound.processStatus()));
|
||||
}
|
||||
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
|
||||
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||
completionGate.requireReleasedBooking(tenantId, inboundId);
|
||||
requirePickingGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
|
||||
requireCabinetEvidence(tenantId, inboundId);
|
||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,23 +87,14 @@ public class LoadingDeliveryService {
|
||||
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
||||
long operatorId, String operatorName,
|
||||
String key, String remark) {
|
||||
return completeAfterPdaSettlement(tenantId, inboundId, tenantId, inboundId,
|
||||
operatorId, operatorName, key, remark);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
||||
long evidenceTenantId, long evidenceInboundId,
|
||||
long operatorId,
|
||||
String operatorName, String key, String remark) {
|
||||
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
||||
long completionInboundId = target == null ? inboundId : target.completionInboundId();
|
||||
Snapshot current = repository.findInboundForUpdate(tenantId, completionInboundId);
|
||||
if (current == null || current.version() == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
|
||||
}
|
||||
return completeLoading(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
|
||||
operatorId, operatorName, key, new CompleteLoadingRequest(current.version(), remark), true);
|
||||
return completeLoading(tenantId, inboundId, operatorId, operatorName, key,
|
||||
new CompleteLoadingRequest(current.version(), remark), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,29 +102,22 @@ public class LoadingDeliveryService {
|
||||
* every external/business hard gate must pass before the PDA task is mutated.
|
||||
*/
|
||||
public void requirePdaCompletionReady(long tenantId, long inboundId) {
|
||||
requirePdaCompletionReady(tenantId, inboundId, tenantId, inboundId);
|
||||
}
|
||||
|
||||
public void requirePdaCompletionReady(long tenantId, long inboundId,
|
||||
long evidenceTenantId, long evidenceInboundId) {
|
||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if (!"LOADING".equals(inbound.processStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "PDA确认装柜完成前订单必须处于LOADING");
|
||||
}
|
||||
loadingModelBridge.requireStarted(tenantId, inboundId);
|
||||
completionGate.requireReleasedBooking(tenantId, inboundId);
|
||||
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
|
||||
}
|
||||
|
||||
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long inboundId,
|
||||
long evidenceTenantId, long evidenceInboundId,
|
||||
long operatorId, String operatorName, String key,
|
||||
CompleteLoadingRequest request,
|
||||
CompletionTarget target,
|
||||
boolean acceptedPdaCompletion) {
|
||||
if ("LOADED".equals(target.loadedChildStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
if (!"LOADING".equals(target.loadedChildStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
|
||||
@@ -216,52 +125,14 @@ public class LoadingDeliveryService {
|
||||
requireLoadingPlanGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
|
||||
completionGate.requireReleasedBooking(tenantId, target.sourceInboundId());
|
||||
requirePickingGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
|
||||
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
|
||||
requireCabinetEvidence(tenantId, target.sourceInboundId());
|
||||
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
|
||||
request.expectedVersion()) != 1) throw concurrent();
|
||||
repository.aggregateRootLoaded(tenantId, target.rootInboundId(), operatorId);
|
||||
addHistory(tenantId, target.completionInboundId(), "LOADING", "LOADED", COMPLETE_LOADING,
|
||||
request.remark(), operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
|
||||
public EvidenceListResponse evidence(long tenantId, long inboundId) {
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
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) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, VOID_EVIDENCE, key, hash);
|
||||
if (replay != null) return replay;
|
||||
requireMutableLoadingStage(tenantId, inboundId);
|
||||
LoadingEvidenceEntity target = repository.listEvidence(tenantId, inboundId).stream()
|
||||
.filter(item -> Long.valueOf(evidenceId).equals(item.getId())).findFirst().orElse(null);
|
||||
if (target == null) throw new BizException(ResultCode.NOT_FOUND, "装柜证据不存在");
|
||||
if (!isActive(target)) throw new BizException(ResultCode.CONFLICT, "装柜证据已经作废");
|
||||
if (repository.voidEvidence(tenantId, inboundId, evidenceId, request.reason().trim(), operatorId,
|
||||
operatorName, request.expectedVersion()) != 1) throw concurrent();
|
||||
LoadingActionResponse result = new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, VOID_EVIDENCE, key, result);
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -287,7 +158,7 @@ public class LoadingDeliveryService {
|
||||
operatorId, operatorName, request.remark(), request.expectedCount(), request.pickedCount(), request.expectedVersion()) != 1) throw concurrent();
|
||||
PickingResponse result = pickResponse(tenantId, inboundId);
|
||||
complete(tenantId, inboundId, operatorId, "START_PICKING", key,
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -312,7 +183,7 @@ public class LoadingDeliveryService {
|
||||
task.getStatus(), PickingStatus.COMPLETED, operatorId, operatorName, request.remark(), request.expectedCount(), request.pickedCount(), request.expectedVersion()) != 1) throw concurrent();
|
||||
PickingResponse result = pickResponse(tenantId, inboundId);
|
||||
complete(tenantId, inboundId, operatorId, "COMPLETE_PICKING", key,
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -378,35 +249,9 @@ public class LoadingDeliveryService {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCompletionEvidence(long tenantId, long inboundId) {
|
||||
Map<String, LoadingEvidenceEntity> values = repository.listEvidence(tenantId, inboundId).stream()
|
||||
.filter(LoadingDeliveryService::isActive)
|
||||
.collect(Collectors.toMap(LoadingEvidenceEntity::getEvidenceType, Function.identity(), (left, right) -> left));
|
||||
boolean filesReady = LoadingEvidenceType.VALUES.stream()
|
||||
.allMatch(type -> values.get(type) != null && values.get(type).getFileId() != null);
|
||||
LoadingEvidenceEntity seal = values.get(LoadingEvidenceType.SEAL);
|
||||
if (!filesReady || seal == null || !StringUtils.hasText(seal.getSealNo())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "空仓、半仓、满仓、锁仓、带锁码照及锁码必须齐全");
|
||||
}
|
||||
LoadingEvidenceType.VALUES.forEach(type -> {
|
||||
LoadingEvidenceEntity evidence = values.get(type);
|
||||
validateFile(tenantId, inboundId, evidence.getFileId(), type);
|
||||
});
|
||||
}
|
||||
|
||||
private LoadingEvidenceFileClient.FileMetadata validateFile(long tenantId, long inboundId, long fileId, String evidenceType) {
|
||||
try {
|
||||
var response = files.validate(internalToken, tenantId, inboundId, fileId);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !("LOADING_" + evidenceType).equals(response.data().businessType())
|
||||
|| !StringUtils.hasText(response.data().contentType())
|
||||
|| !response.data().contentType().toLowerCase(java.util.Locale.ROOT).startsWith("image/")) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "证据文件不存在或不属于当前订单");
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException ex) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
private void requireCabinetEvidence(long tenantId, long inboundId) {
|
||||
cabinetEvidenceService.requireComplete(
|
||||
cabinetEvidenceService.requireContainerBookingId(tenantId, inboundId));
|
||||
}
|
||||
|
||||
private Snapshot requireInbound(long tenantId, long inboundId) {
|
||||
@@ -426,44 +271,6 @@ public class LoadingDeliveryService {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Evidence is append-only while loading. Once LOADED is reached this API is read-only;
|
||||
* corrections are handled by the offline/manual process defined for phase three. */
|
||||
private Snapshot requireMutableLoadingStage(long tenantId, long inboundId) {
|
||||
Snapshot value = requireInbound(tenantId, inboundId);
|
||||
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
||||
if (target != null) {
|
||||
if (!"LOADING".equals(target.loadedChildStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜完成后不允许在线修改证据");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (!"LOADING".equals(value.processStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜完成后不允许在线修改证据");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
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(), 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()));
|
||||
}
|
||||
|
||||
private static String activeStatus(LoadingEvidenceEntity evidence) {
|
||||
return evidence.getStatus() == null ? "ACTIVE" : evidence.getStatus();
|
||||
}
|
||||
|
||||
private LoadingActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key, String hash) {
|
||||
return claimOrReplay(tenantId, inboundId, operatorId, operation, key, hash, false);
|
||||
|
||||
@@ -149,7 +149,7 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_plan WHERE container_booking_id=#{id}") int hasLoadingPlan(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0") int hasActiveLoadingItems(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id WHERE p.container_booking_id=#{id}") int hasPickTask(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_cabinet_evidence e WHERE e.container_booking_id=#{id} AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_container_document WHERE container_booking_id=#{id}") int hasDocuments(@Param("id") long id);
|
||||
@Update("""
|
||||
UPDATE fl_container_booking SET bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.List;
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_pick_item WHERE pick_task_id=#{id} AND item_status IN ('PICKED','PARTIAL','SKIPPED')") int completedItemCount(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_pick_item WHERE pick_task_id=#{id} AND item_status NOT IN ('PICKED','PARTIAL','SKIPPED')") int unresolvedItemCount(@Param("id") long id);
|
||||
@Select("SELECT DISTINCT h.operation_reason FROM fl_loading_pick_history h WHERE h.pick_task_id=#{id} AND h.operation_reason IS NOT NULL AND h.operation_reason<>'' ORDER BY h.id") List<String> pickExceptionReasons(@Param("id") long id);
|
||||
@Select("SELECT DISTINCT e.id evidenceId,e.tenant_id tenantId,e.inbound_id inboundId,e.evidence_type evidenceType,e.file_id fileId,e.file_name fileName,e.status,e.created_at uploadedAt FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id WHERE i.loading_plan_id=#{id} AND i.deleted=0 ORDER BY e.created_at,e.id") List<PhotoRow> photos(@Param("id") long id);
|
||||
@Select("SELECT e.id evidenceId,NULL tenantId,NULL inboundId,e.evidence_type evidenceType,e.file_id fileId,e.file_name fileName,e.status,e.created_at uploadedAt FROM fl_loading_cabinet_evidence e JOIN fl_loading_plan p ON p.container_booking_id=e.container_booking_id WHERE p.id=#{id} ORDER BY e.created_at,e.id") List<PhotoRow> photos(@Param("id") long id);
|
||||
@Select("SELECT action_type eventType,operation_id detail,operator_id operatorId,operator_name operatorName,created_at occurredAt FROM fl_loading_pick_operation WHERE pick_task_id=#{id} ORDER BY created_at,id") List<TimelineRow> pickTimeline(@Param("id") long id);
|
||||
record ExceptionRow(Long id,String action,String reason,Long operatorId,String operatorName,java.time.LocalDateTime occurredAt,String requestHash) {}
|
||||
record PickTaskRow(Long id,Long loadingPlanId,Long containerBookingId,String taskStatus,Integer version) {}
|
||||
|
||||
@@ -278,11 +278,10 @@ public interface LoadingListQueryMapper {
|
||||
WHERE task.loading_plan_id=#{planId}
|
||||
AND (task.task_status<>'LOADING' OR item.item_status<>'PENDING'
|
||||
OR item.picked_package_count<>0))
|
||||
OR EXISTS (SELECT 1 FROM fl_loading_evidence evidence
|
||||
JOIN fl_loading_plan_item plan_item
|
||||
ON plan_item.account_id=evidence.tenant_id AND plan_item.inbound_id=evidence.inbound_id
|
||||
WHERE plan_item.loading_plan_id=#{planId} AND plan_item.deleted=0
|
||||
AND evidence.status='ACTIVE')
|
||||
OR EXISTS (SELECT 1 FROM fl_loading_cabinet_evidence evidence
|
||||
JOIN fl_loading_plan evidence_plan
|
||||
ON evidence_plan.container_booking_id=evidence.container_booking_id
|
||||
WHERE evidence_plan.id=#{planId} AND evidence.status='ACTIVE')
|
||||
THEN 1 ELSE 0 END
|
||||
""")
|
||||
int hasPdaWorkStarted(@Param("planId") long planId);
|
||||
|
||||
+6
-4
@@ -70,9 +70,7 @@ public interface OutboundDispatchReadMapper {
|
||||
AND evidence.file_id IS NOT NULL AND evidence.seal_no IS NOT NULL
|
||||
AND evidence.seal_no<>'' THEN 1 ELSE 0 END) validSeal
|
||||
FROM fl_loading_plan plan
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=plan.id AND item.deleted=0
|
||||
JOIN fl_loading_evidence evidence ON evidence.inbound_id=item.inbound_id
|
||||
AND evidence.tenant_id=item.account_id
|
||||
JOIN fl_loading_cabinet_evidence evidence ON evidence.container_booking_id=plan.container_booking_id
|
||||
WHERE plan.id=#{loadingPlanId} AND plan.container_booking_id=#{containerBookingId}
|
||||
""")
|
||||
Evidence evidence(@Param("containerBookingId") long containerBookingId,
|
||||
@@ -95,7 +93,11 @@ public interface OutboundDispatchReadMapper {
|
||||
COALESCE(e.updated_by,e.created_by),COALESCE(e.updated_by_name,e.created_by_name),
|
||||
CONCAT(e.evidence_type,CASE WHEN e.seal_no IS NULL OR e.seal_no='' THEN '' ELSE CONCAT(':',e.seal_no) END),
|
||||
COALESCE(e.updated_at,e.created_at)
|
||||
FROM fl_loading_evidence e WHERE e.tenant_id=#{tenantId} AND e.inbound_id=#{inboundId}
|
||||
FROM fl_loading_cabinet_evidence e
|
||||
JOIN fl_loading_plan evidence_plan ON evidence_plan.container_booking_id=e.container_booking_id
|
||||
JOIN fl_loading_plan_item evidence_item ON evidence_item.loading_plan_id=evidence_plan.id
|
||||
AND evidence_item.deleted=0
|
||||
WHERE evidence_item.account_id=#{tenantId} AND evidence_item.inbound_id=#{inboundId}
|
||||
UNION ALL
|
||||
SELECT 'OCEAN_BOOKING',h.action,h.from_status,h.to_status,h.operator_id,h.operator_name,h.reason,h.created_at
|
||||
FROM fl_ocean_booking_status_history h WHERE h.ocean_booking_id=#{oceanBookingId}
|
||||
|
||||
+10
-14
@@ -1,9 +1,9 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.CabinetLoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import com.baiyue.firstleg.split.mapper.PreloadPlanMapper;
|
||||
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
|
||||
public class UnifiedInboundReadRepository {
|
||||
@Resource private PreloadPlanMapper preloads;
|
||||
@Resource private InboundLoadingPickTaskMapper picks;
|
||||
@Resource private LoadingEvidenceMapper evidences;
|
||||
@Resource private CabinetLoadingEvidenceMapper evidences;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private UnifiedInboundTransportMapper transports;
|
||||
|
||||
@@ -42,11 +42,8 @@ public class UnifiedInboundReadRepository {
|
||||
.eq(InboundLoadingPickTaskEntity::getInboundId, inboundId));
|
||||
}
|
||||
|
||||
public List<LoadingEvidenceEntity> findEvidence(long tenantId, long inboundId) {
|
||||
return evidences.selectList(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.orderByAsc(LoadingEvidenceEntity::getEvidenceType));
|
||||
public List<CabinetLoadingEvidenceEntity> findEvidence(long tenantId, long inboundId) {
|
||||
return evidences.byInbound(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public Map<Long, PreloadPlanEntity> findPreloads(Map<Long, Long> tenantByInbound) {
|
||||
@@ -65,13 +62,12 @@ public class UnifiedInboundReadRepository {
|
||||
.collect(Collectors.toMap(InboundLoadingPickTaskEntity::getInboundId, Function.identity(), (left, right) -> left));
|
||||
}
|
||||
|
||||
public Map<Long, List<LoadingEvidenceEntity>> findEvidence(Map<Long, Long> tenantByInbound) {
|
||||
public Map<Long, List<CabinetLoadingEvidenceEntity>> findEvidence(Map<Long, Long> tenantByInbound) {
|
||||
if (tenantByInbound.isEmpty()) return Map.of();
|
||||
return evidences.selectList(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.in(LoadingEvidenceEntity::getInboundId, tenantByInbound.keySet())
|
||||
.orderByAsc(LoadingEvidenceEntity::getEvidenceType))
|
||||
.stream().filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||
.collect(Collectors.groupingBy(LoadingEvidenceEntity::getInboundId));
|
||||
Map<Long, List<CabinetLoadingEvidenceEntity>> result = new java.util.HashMap<>();
|
||||
tenantByInbound.forEach((inboundId, tenantId) -> result.put(inboundId,
|
||||
evidences.byInbound(tenantId, inboundId)));
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<Long, InboundStatusHistoryEntity> findDeliveryHistory(Map<Long, Long> tenantByInbound) {
|
||||
|
||||
@@ -134,7 +134,7 @@ public class ContainerOverviewService {
|
||||
.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);
|
||||
response = evidenceFiles.download(internalToken, containerBookingId, photo.evidenceType(), fileId);
|
||||
} catch (FeignException.NotFound exception) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA照片文件不存在或归属不匹配");
|
||||
} catch (FeignException exception) {
|
||||
|
||||
@@ -96,6 +96,8 @@ public class LoadingPickService {
|
||||
@Resource
|
||||
private LoadingDeliveryService loadingDeliveryService;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.loading.service.CabinetLoadingEvidenceService cabinetEvidenceService;
|
||||
@Resource
|
||||
private ColoadGroupAssignmentPort coloadGroups;
|
||||
@Resource
|
||||
private TruckAppointmentService truckAppointmentService;
|
||||
@@ -520,9 +522,10 @@ public class LoadingPickService {
|
||||
.toList();
|
||||
if (owners.isEmpty()) throw new CompletionProcessingException("OWNER_DISCOVERY", conflict("PDA任务未关联有效入库单"));
|
||||
List<GroupSnapshot> taskGroups = coloadGroups(owners);
|
||||
LoadingPickItemMapper.InboundOwner evidenceOwner = owners.get(0);
|
||||
runCompletionStage("CABINET_EVIDENCE", () ->
|
||||
cabinetEvidenceService.requireComplete(task.getContainerBookingId()));
|
||||
|
||||
// Phase 1 is read-only: every owner must pass its business gates against the same cabinet evidence owner
|
||||
// Phase 1 is read-only: cabinet evidence is checked once, then every owner must pass its business gates
|
||||
// before any warehouse resource or inbound status can be changed. This prevents a later customer gate from
|
||||
// leaving earlier customers partially completed.
|
||||
for (int index = 0; index < owners.size(); index++) {
|
||||
@@ -531,7 +534,7 @@ public class LoadingPickService {
|
||||
String checkpoint = completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId());
|
||||
if (!checkpointAtLeast(checkpoint, "SETTLED")) {
|
||||
runCompletionStage("OWNER_READY", () -> loadingDeliveryService.requirePdaCompletionReady(
|
||||
owner.tenantId(), owner.inboundId(), evidenceOwner.tenantId(), evidenceOwner.inboundId()));
|
||||
owner.tenantId(), owner.inboundId()));
|
||||
if (!checkpointAtLeast(checkpoint, "READY")) {
|
||||
requireCompletionLease(command.getId(), worker);
|
||||
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(),
|
||||
@@ -554,7 +557,7 @@ public class LoadingPickService {
|
||||
}
|
||||
if (!checkpointAtLeast(checkpoint, "INBOUND_COMPLETED")) {
|
||||
runCompletionStage("INBOUND_COMPLETE", () -> loadingDeliveryService.completeAfterPdaSettlement(
|
||||
owner.tenantId(), owner.inboundId(), evidenceOwner.tenantId(), evidenceOwner.inboundId(),
|
||||
owner.tenantId(), owner.inboundId(),
|
||||
command.getOperatorId(), command.getOperatorName(),
|
||||
"PDA-" + command.getOperationId() + '-' + owner.inboundId(), command.getRemark()));
|
||||
requireCompletionLease(command.getId(), worker);
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.baiyue.firstleg.dto.UnifiedInboundQuery;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundSummary;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.dto.CurrentContainerTransport;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
||||
import com.baiyue.firstleg.repository.UnifiedInboundReadRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -69,7 +69,7 @@ public class UnifiedInboundQueryService {
|
||||
// Phase-three tables are optional during the phase-two measurement workflow.
|
||||
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload = null;
|
||||
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick = null;
|
||||
List<LoadingEvidenceEntity> evidence = List.of();
|
||||
List<CabinetLoadingEvidenceEntity> evidence = List.of();
|
||||
LocalDateTime deliveryAt = null;
|
||||
if (includePhaseThree) {
|
||||
preload = operations.findPreload(tenantId, inbound.getId());
|
||||
@@ -116,21 +116,21 @@ public class UnifiedInboundQueryService {
|
||||
private UnifiedInboundSummary summary(InboundOrderEntity inbound, InboundOrderDetail detail,
|
||||
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload,
|
||||
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick,
|
||||
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
|
||||
List<CabinetLoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
|
||||
CurrentContainerTransport transport, String locationCodes,
|
||||
com.baiyue.firstleg.entity.InboundPickupSnapshotEntity pickup) {
|
||||
String mainStatus = mainStatus(inbound.getProcessStatus());
|
||||
List<LoadingEvidenceEntity> activeEvidence = evidence.stream()
|
||||
List<CabinetLoadingEvidenceEntity> activeEvidence = evidence.stream()
|
||||
.filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList();
|
||||
String sealNo = activeEvidence.stream().filter(item -> "SEAL".equals(item.getEvidenceType()))
|
||||
.map(LoadingEvidenceEntity::getSealNo).filter(StringUtils::hasText).findFirst().orElse(null);
|
||||
.map(CabinetLoadingEvidenceEntity::getSealNo).filter(StringUtils::hasText).findFirst().orElse(null);
|
||||
var preloadSummary = preload == null ? null : new UnifiedInboundSummary.PreloadSummary(
|
||||
preload.getStatus(), preload.getVersion(), itemCount(preload.getItemsJson()), preload.getUpdatedAt());
|
||||
var loadingSummary = pick == null && activeEvidence.isEmpty() ? null : new UnifiedInboundSummary.LoadingSummary(
|
||||
pick == null ? null : pick.getStatus(), pick == null ? null : pick.getVersion(),
|
||||
pick == null ? null : pick.getVersion(), pick == null ? null : pick.getExpectedCount(),
|
||||
pick == null ? null : pick.getPickedCount(), activeEvidence.size(), sealNo,
|
||||
pick == null ? activeEvidence.stream().map(LoadingEvidenceEntity::getUpdatedAt)
|
||||
pick == null ? activeEvidence.stream().map(CabinetLoadingEvidenceEntity::getUpdatedAt)
|
||||
.filter(java.util.Objects::nonNull).max(LocalDateTime::compareTo).orElse(null) : pick.getUpdatedAt());
|
||||
var deliverySummary = "LOADED".equals(mainStatus) || "DELIVERED".equals(mainStatus)
|
||||
? new UnifiedInboundSummary.DeliverySummary(
|
||||
|
||||
Reference in New Issue
Block a user