优化
This commit is contained in:
+13
@@ -6,6 +6,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteDeliveryRequest;
|
||||
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;
|
||||
@@ -47,6 +48,18 @@ public class PlatformLoadingDeliveryController {
|
||||
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')")
|
||||
|
||||
@@ -3,18 +3,18 @@ package com.baiyue.firstleg.loading.domain;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 装柜证据类型。照片语义尚待产品确认,当前只固定四个步骤和一个铅封证据槽位。
|
||||
* 装柜证据类型。接口保留稳定英文编码,业务含义已确认:空仓、半仓、满仓、锁仓、带锁码照。
|
||||
*/
|
||||
public final class LoadingEvidenceType {
|
||||
/** 装柜过程第 1 步照片。 */
|
||||
/** 空仓照片。 */
|
||||
public static final String PHOTO_STEP_1 = "PHOTO_STEP_1";
|
||||
/** 装柜过程第 2 步照片。 */
|
||||
/** 半仓照片。 */
|
||||
public static final String PHOTO_STEP_2 = "PHOTO_STEP_2";
|
||||
/** 装柜过程第 3 步照片。 */
|
||||
/** 满仓照片。 */
|
||||
public static final String PHOTO_STEP_3 = "PHOTO_STEP_3";
|
||||
/** 装柜过程第 4 步照片。 */
|
||||
/** 锁仓照片。 */
|
||||
public static final String PHOTO_STEP_4 = "PHOTO_STEP_4";
|
||||
/** 柜门铅封或封条证据。 */
|
||||
/** 带锁码照片;请求必须同时提供 sealNo。 */
|
||||
public static final String SEAL = "SEAL";
|
||||
public static final Set<String> VALUES = Set.of(PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL);
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ public final class LoadingRequests {
|
||||
@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) {}
|
||||
|
||||
@@ -8,7 +8,9 @@ public final class LoadingResponses {
|
||||
|
||||
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
|
||||
Long fileId, String fileName, String sealNo, String remark,
|
||||
Integer version, LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
Integer version, String status, String voidReason,
|
||||
Long voidedBy, String voidedByName, LocalDateTime voidedAt,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record EvidenceListResponse(Long inboundId, List<EvidenceResponse> evidences) {}
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ public class LoadingEvidenceEntity {
|
||||
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 String voidReason;
|
||||
private Long voidedBy;
|
||||
private String voidedByName;
|
||||
private LocalDateTime voidedAt;
|
||||
private Integer version;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
|
||||
+9
-9
@@ -8,13 +8,13 @@ import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingEvidenceMapper extends BaseMapper<LoadingEvidenceEntity> {
|
||||
@Update("UPDATE fl_loading_evidence SET file_id=#{fileId}, file_name=#{fileName}, seal_no=#{sealNo}, "
|
||||
+ "remark=#{remark}, updated_by=#{operatorId}, updated_by_name=#{operatorName}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND evidence_type=#{evidenceType} "
|
||||
+ "AND version=#{expectedVersion}")
|
||||
int updateEvidence(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("evidenceType") String evidenceType, @Param("fileId") Long fileId,
|
||||
@Param("fileName") String fileName, @Param("sealNo") String sealNo,
|
||||
@Param("remark") String remark, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName, @Param("expectedVersion") int expectedVersion);
|
||||
@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);
|
||||
}
|
||||
|
||||
+4
-11
@@ -33,13 +33,6 @@ public class LoadingRepository {
|
||||
return inbounds.completeDelivery(tenantId, inboundId, operatorId, expectedVersion);
|
||||
}
|
||||
|
||||
public LoadingEvidenceEntity findEvidence(long tenantId, long inboundId, String type) {
|
||||
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.eq(LoadingEvidenceEntity::getEvidenceType, type));
|
||||
}
|
||||
|
||||
public List<LoadingEvidenceEntity> listEvidence(long tenantId, long inboundId) {
|
||||
return evidences.selectList(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
@@ -51,15 +44,15 @@ public class LoadingRepository {
|
||||
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getEvidenceType, "SEAL")
|
||||
.eq(LoadingEvidenceEntity::getStatus, "ACTIVE")
|
||||
.eq(LoadingEvidenceEntity::getSealNo, sealNo));
|
||||
}
|
||||
|
||||
public void createEvidence(LoadingEvidenceEntity entity) { evidences.insert(entity); }
|
||||
|
||||
public int updateEvidence(long tenantId, long inboundId, String type, Long fileId, String fileName,
|
||||
String sealNo, String remark, long operatorId, String operatorName, int version) {
|
||||
return evidences.updateEvidence(tenantId, inboundId, type, fileId, fileName, sealNo, remark,
|
||||
operatorId, operatorName, version);
|
||||
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,
|
||||
|
||||
+43
-19
@@ -39,6 +39,7 @@ 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";
|
||||
private static final String COMPLETE_DELIVERY = "COMPLETE_DELIVERY";
|
||||
|
||||
@@ -74,23 +75,17 @@ public class LoadingDeliveryService {
|
||||
String fileName = file.fileName();
|
||||
String sealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType()) ? request.sealNo().trim() : null;
|
||||
try {
|
||||
LoadingEvidenceEntity existing = repository.findEvidence(tenantId, inboundId, request.evidenceType());
|
||||
if (existing == null) {
|
||||
if (request.expectedVersion() != 0) throw concurrent();
|
||||
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.setVersion(0); entity.setCreatedBy(operatorId); entity.setCreatedByName(operatorName);
|
||||
entity.setUpdatedBy(operatorId); entity.setUpdatedByName(operatorName);
|
||||
repository.createEvidence(entity);
|
||||
} else if (repository.updateEvidence(tenantId, inboundId, request.evidenceType(), request.fileId(), fileName,
|
||||
sealNo, request.remark(), operatorId, operatorName, request.expectedVersion()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
// 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, LoadingEvidenceType.SEAL.equals(request.evidenceType())
|
||||
? "锁码已用于其他装柜任务" : "数据已被其他人修改,请刷新后重试");
|
||||
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);
|
||||
@@ -141,6 +136,23 @@ public class LoadingDeliveryService {
|
||||
return evidenceResponse(tenantId, inboundId);
|
||||
}
|
||||
|
||||
@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;
|
||||
requireLoadingStage(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);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PickingResponse startPicking(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String key, PickingRequest request) {
|
||||
@@ -242,7 +254,8 @@ public class LoadingDeliveryService {
|
||||
|
||||
private void requireCompletionEvidence(long tenantId, long inboundId) {
|
||||
Map<String, LoadingEvidenceEntity> values = repository.listEvidence(tenantId, inboundId).stream()
|
||||
.collect(Collectors.toMap(LoadingEvidenceEntity::getEvidenceType, Function.identity(), (left, right) -> right));
|
||||
.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);
|
||||
@@ -259,7 +272,9 @@ public class LoadingDeliveryService {
|
||||
try {
|
||||
var response = files.validate(internalToken, tenantId, inboundId, fileId);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !("LOADING_" + evidenceType).equals(response.data().businessType())) {
|
||||
|| !("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();
|
||||
@@ -286,10 +301,19 @@ 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(), e.getCreatedAt(), e.getUpdatedAt())).toList();
|
||||
e.getFileName(), 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 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) {
|
||||
validateKey(key);
|
||||
|
||||
+3
-3
@@ -55,9 +55,9 @@ public interface OutboundDispatchReadMapper {
|
||||
@Param("customerId") long customerId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(DISTINCT CASE WHEN file_id IS NOT NULL AND evidence_type IN
|
||||
SELECT COUNT(DISTINCT CASE WHEN status='ACTIVE' AND file_id IS NOT NULL AND evidence_type IN
|
||||
('PHOTO_STEP_1','PHOTO_STEP_2','PHOTO_STEP_3','PHOTO_STEP_4','SEAL') THEN evidence_type END) evidenceCount,
|
||||
MAX(CASE WHEN evidence_type='SEAL' AND file_id IS NOT NULL AND seal_no IS NOT NULL
|
||||
MAX(CASE WHEN status='ACTIVE' AND evidence_type='SEAL' AND file_id IS NOT NULL AND seal_no IS NOT NULL
|
||||
AND seal_no<>'' THEN 1 ELSE 0 END) validSeal
|
||||
FROM fl_loading_evidence WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId}
|
||||
""")
|
||||
@@ -88,7 +88,7 @@ public interface OutboundDispatchReadMapper {
|
||||
FROM fl_loading_pick_history h JOIN fl_loading_pick_task task ON task.id=h.pick_task_id
|
||||
WHERE task.loading_plan_id=#{loadingPlanId}
|
||||
UNION ALL
|
||||
SELECT 'LOADING_EVIDENCE','UPSERT_EVIDENCE',NULL,NULL,
|
||||
SELECT 'LOADING_EVIDENCE',CASE WHEN e.status='VOID' THEN 'VOID_EVIDENCE' ELSE 'UPSERT_EVIDENCE' END,NULL,NULL,
|
||||
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)
|
||||
|
||||
@@ -104,15 +104,17 @@ public class UnifiedInboundQueryService {
|
||||
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
|
||||
InboundTransportSnapshotEntity transport) {
|
||||
String mainStatus = mainStatus(inbound.getProcessStatus());
|
||||
String sealNo = evidence.stream().filter(item -> "SEAL".equals(item.getEvidenceType()))
|
||||
List<LoadingEvidenceEntity> 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);
|
||||
var preloadSummary = preload == null ? null : new UnifiedInboundSummary.PreloadSummary(
|
||||
preload.getStatus(), preload.getVersion(), itemCount(preload.getItemsJson()), preload.getUpdatedAt());
|
||||
var loadingSummary = pick == null && evidence.isEmpty() ? null : new UnifiedInboundSummary.LoadingSummary(
|
||||
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(), evidence.size(), sealNo,
|
||||
pick == null ? evidence.stream().map(LoadingEvidenceEntity::getUpdatedAt)
|
||||
pick == null ? null : pick.getPickedCount(), activeEvidence.size(), sealNo,
|
||||
pick == null ? activeEvidence.stream().map(LoadingEvidenceEntity::getUpdatedAt)
|
||||
.filter(java.util.Objects::nonNull).max(LocalDateTime::compareTo).orElse(null) : pick.getUpdatedAt());
|
||||
var deliverySummary = "LOADED".equals(mainStatus) || "DELIVERED".equals(mainStatus)
|
||||
? new UnifiedInboundSummary.DeliverySummary(
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
/* 装柜证据、PDA 拣货复核和派送完成。业务服务强制五类真实文件证据齐全后才能完成装柜。 */
|
||||
/* 装柜证据、PDA 拣货复核和派送完成。五类证据按有效记录计数,同类型允许多图追加;作废记录保留审计。 */
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_evidence (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL',
|
||||
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID',
|
||||
file_name VARCHAR(255) NULL,
|
||||
seal_no VARCHAR(64) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/VOID,作废不硬删除',
|
||||
void_reason VARCHAR(500) NULL,
|
||||
voided_by BIGINT UNSIGNED NULL,
|
||||
voided_by_name VARCHAR(64) NULL,
|
||||
voided_at DATETIME(3) NULL,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
@@ -19,11 +24,74 @@ CREATE TABLE IF NOT EXISTS fl_loading_evidence (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type),
|
||||
UNIQUE KEY uk_loading_evidence_seal (tenant_id, seal_no),
|
||||
KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status),
|
||||
KEY idx_loading_evidence_seal (tenant_id, seal_no, status),
|
||||
KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at),
|
||||
CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据';
|
||||
) ENGINE=InnoDB COMMENT='订单装柜空仓/半仓/满仓/锁仓/带锁码照证据';
|
||||
|
||||
/* Repeatable migration for installations that already created the single-slot table. */
|
||||
DROP PROCEDURE IF EXISTS phase3_loading_add_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase3_loading_add_column(IN p_column VARCHAR(64), IN p_definition VARCHAR(512))
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND column_name=p_column
|
||||
) THEN
|
||||
SET @phase3_loading_ddl = CONCAT('ALTER TABLE fl_loading_evidence ADD COLUMN `', p_column, '` ', p_definition);
|
||||
PREPARE phase3_loading_stmt FROM @phase3_loading_ddl;
|
||||
EXECUTE phase3_loading_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''ACTIVE/VOID,作废不硬删除''');
|
||||
CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL');
|
||||
CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL');
|
||||
CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL');
|
||||
CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL');
|
||||
DROP PROCEDURE IF EXISTS phase3_loading_add_column;
|
||||
|
||||
SET @phase3_loading_has_legacy_type_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_type'
|
||||
);
|
||||
SET @phase3_loading_drop_legacy_type_index = IF(@phase3_loading_has_legacy_type_index > 0,
|
||||
'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_type', 'SELECT 1');
|
||||
PREPARE phase3_loading_drop_stmt FROM @phase3_loading_drop_legacy_type_index;
|
||||
EXECUTE phase3_loading_drop_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_drop_stmt;
|
||||
|
||||
SET @phase3_loading_has_type_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='idx_loading_evidence_type'
|
||||
);
|
||||
SET @phase3_loading_add_type_index = IF(@phase3_loading_has_type_index = 0,
|
||||
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status)', 'SELECT 1');
|
||||
PREPARE phase3_loading_type_stmt FROM @phase3_loading_add_type_index;
|
||||
EXECUTE phase3_loading_type_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_type_stmt;
|
||||
|
||||
SET @phase3_loading_has_legacy_seal_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_seal'
|
||||
);
|
||||
SET @phase3_loading_drop_legacy_seal_index = IF(@phase3_loading_has_legacy_seal_index > 0,
|
||||
'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_seal', 'SELECT 1');
|
||||
PREPARE phase3_loading_drop_seal_stmt FROM @phase3_loading_drop_legacy_seal_index;
|
||||
EXECUTE phase3_loading_drop_seal_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_drop_seal_stmt;
|
||||
|
||||
SET @phase3_loading_has_seal_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='idx_loading_evidence_seal'
|
||||
);
|
||||
SET @phase3_loading_add_seal_index = IF(@phase3_loading_has_seal_index = 0,
|
||||
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_seal (tenant_id, seal_no, status)', 'SELECT 1');
|
||||
PREPARE phase3_loading_seal_stmt FROM @phase3_loading_add_seal_index;
|
||||
EXECUTE phase3_loading_seal_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_seal_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
@@ -10,7 +10,7 @@ paths:
|
||||
responses:
|
||||
'200': {description: Business envelope containing the stable evidence array}
|
||||
post:
|
||||
summary: Bind one real File service object to a loading evidence slot
|
||||
summary: Append one real File service object to a loading evidence type
|
||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -18,6 +18,16 @@ paths:
|
||||
application/json: {schema: {$ref: '#/components/schemas/UpsertEvidenceRequest'}}
|
||||
responses:
|
||||
'200': {description: Business envelope; ownership, type and version failures use business error codes}
|
||||
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void:
|
||||
post:
|
||||
summary: Void an incorrect loading evidence photo without hard deletion
|
||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/EvidenceId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json: {schema: {$ref: '#/components/schemas/VoidEvidenceRequest'}}
|
||||
responses:
|
||||
'200': {description: Business envelope containing the voided record and audit fields}
|
||||
/api/platform/v1/inbounds/{inboundId}/loading/picking/start:
|
||||
post:
|
||||
summary: Refresh the compatibility projection from canonical container PDA tasks
|
||||
@@ -50,7 +60,7 @@ paths:
|
||||
'200': {description: Business envelope containing task or null}
|
||||
/api/platform/v1/inbounds/{inboundId}/loading/complete:
|
||||
post:
|
||||
summary: Complete loading only after picking and PHOTO_STEP_1..4 plus SEAL evidence are complete
|
||||
summary: Complete loading only after picking and five evidence types each have an active photo
|
||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -62,6 +72,7 @@ components:
|
||||
parameters:
|
||||
InboundId: {name: inboundId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
TenantId: {name: tenantId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
EvidenceId: {name: evidenceId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
schemas:
|
||||
UpsertEvidenceRequest:
|
||||
@@ -69,11 +80,17 @@ components:
|
||||
required: [expectedVersion, evidenceType, fileId]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}
|
||||
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL], description: PHOTO_STEP_1=空仓,PHOTO_STEP_2=半仓,PHOTO_STEP_3=满仓,PHOTO_STEP_4=锁仓,SEAL=带锁码照;同类型可多次追加}
|
||||
fileId: {type: integer, format: int64, minimum: 1}
|
||||
fileName: {type: string, maxLength: 255, deprecated: true, description: Ignored; server uses File metadata}
|
||||
sealNo: {type: string, maxLength: 64, description: Required when evidenceType is SEAL}
|
||||
remark: {type: string, maxLength: 1000}
|
||||
VoidEvidenceRequest:
|
||||
type: object
|
||||
required: [expectedVersion, reason]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
reason: {type: string, minLength: 1, maxLength: 500, description: 作废原因,记录操作人和时间}
|
||||
PickingRequest:
|
||||
type: object
|
||||
required: [expectedVersion, expectedCount, pickedCount]
|
||||
|
||||
+2
@@ -15,11 +15,13 @@ class LoadingDeliveryOpenApiContractTest {
|
||||
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/picking/start:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/picking/complete:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/complete:")
|
||||
.contains("required: [expectedVersion, evidenceType, fileId]")
|
||||
.contains("enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]")
|
||||
.contains("同类型可多次追加")
|
||||
.contains("Required when evidenceType is SEAL")
|
||||
.contains("compatibility projection")
|
||||
.contains("must exactly match the server-derived container-task aggregate")
|
||||
|
||||
+13
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteDeliveryRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.VoidEvidenceRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -27,6 +28,7 @@ class PlatformLoadingDeliveryControllerContractTest {
|
||||
"platform:firstleg:inbound:loading:edit", UpsertEvidenceRequest.class);
|
||||
get("evidence", "/{inboundId}/loading/evidence",
|
||||
"platform:firstleg:inbound:loading:view");
|
||||
postVoidEvidence();
|
||||
post("completeLoading", "/{inboundId}/loading/complete",
|
||||
"platform:firstleg:inbound:loading:complete", CompleteLoadingRequest.class);
|
||||
post("completeDelivery", "/{inboundId}/delivery/complete",
|
||||
@@ -39,6 +41,17 @@ class PlatformLoadingDeliveryControllerContractTest {
|
||||
"platform:firstleg:inbound:picking:view");
|
||||
}
|
||||
|
||||
private static void postVoidEvidence() throws Exception {
|
||||
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod("voidEvidence",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class, String.class, VoidEvidenceRequest.class);
|
||||
assertThat(method.getAnnotation(PostMapping.class).value())
|
||||
.containsExactly("/{inboundId}/loading/evidence/{evidenceId}/void");
|
||||
assertAuthority(method, "platform:firstleg:inbound:loading:edit");
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[4].getAnnotation(RequestHeader.class).value())
|
||||
.isEqualTo("Idempotency-Key");
|
||||
}
|
||||
|
||||
private static void post(String name, String path, String authority, Class<?> requestType) throws Exception {
|
||||
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod(name,
|
||||
AuthenticatedUser.class, long.class, long.class, String.class, requestType);
|
||||
|
||||
+9
-3
@@ -9,14 +9,20 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LoadingEvidenceSqlContractTest {
|
||||
@Test
|
||||
void newSchemaRequiresRealFilesAndUniqueSealWithinCustomer() throws Exception {
|
||||
void schemaSupportsAppendOnlyEvidenceAndVoidAudit() throws Exception {
|
||||
String sql = new ClassPathResource("db/phase3-loading-delivery-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("file_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type)")
|
||||
.contains("UNIQUE KEY uk_loading_evidence_seal (tenant_id, seal_no)")
|
||||
.contains("KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status)")
|
||||
.contains("status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'")
|
||||
.contains("void_reason VARCHAR(500)")
|
||||
.contains("voided_by BIGINT UNSIGNED")
|
||||
.contains("voided_at DATETIME(3)")
|
||||
.contains("DROP INDEX uk_loading_evidence_type")
|
||||
.contains("DROP INDEX uk_loading_evidence_seal")
|
||||
.contains("idx_loading_evidence_seal (tenant_id, seal_no, status)")
|
||||
.contains("fl_inbound_loading_pick_task");
|
||||
}
|
||||
}
|
||||
|
||||
+37
-1
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.VoidEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.LoadingActionResponse;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.Snapshot;
|
||||
@@ -98,7 +99,6 @@ class LoadingDeliveryServiceTest {
|
||||
void evidenceCanBeEditedAndDoesNotChangeMainStatus() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
when(repository.findEvidence(10, 20, "PHOTO_STEP_1")).thenReturn(null);
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of());
|
||||
when(files.validate("token", 10, 20, 1)).thenReturn(ApiResponse.success(
|
||||
new LoadingEvidenceFileClient.FileMetadata(1L, "photo.jpg", "LOADING_PHOTO_STEP_1", "image/jpeg")));
|
||||
@@ -109,6 +109,42 @@ class LoadingDeliveryServiceTest {
|
||||
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendsMultiplePhotosOfTheSameType() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of());
|
||||
when(files.validate(eq("token"), eq(10L), eq(20L), anyLong())).thenAnswer(invocation -> {
|
||||
long id = invocation.getArgument(3);
|
||||
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(id, "photo-" + id + ".jpg",
|
||||
"LOADING_PHOTO_STEP_1", "image/jpeg"));
|
||||
});
|
||||
|
||||
service.upsertEvidence(10, 20, 7, "admin", "append-1",
|
||||
new UpsertEvidenceRequest(0, "PHOTO_STEP_1", 11L, null, null, null));
|
||||
service.upsertEvidence(10, 20, 7, "admin", "append-2",
|
||||
new UpsertEvidenceRequest(0, "PHOTO_STEP_1", 12L, null, null, null));
|
||||
|
||||
verify(repository, times(2)).createEvidence(argThat(value ->
|
||||
"PHOTO_STEP_1".equals(value.getEvidenceType()) && "ACTIVE".equals(value.getStatus())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void voidsPhotoWithReasonAndKeepsTheRecord() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
LoadingEvidenceEntity target = evidence("PHOTO_STEP_1", 11L, null);
|
||||
target.setId(99L); target.setStatus("ACTIVE"); target.setVersion(2);
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of(target));
|
||||
when(repository.voidEvidence(10, 20, 99L, "模糊", 7L, "admin", 2)).thenReturn(1);
|
||||
|
||||
LoadingActionResponse result = service.voidEvidence(10, 20, 99L, 7L, "admin", "void-1",
|
||||
new VoidEvidenceRequest(2, "模糊"));
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(repository).voidEvidence(10, 20, 99L, "模糊", 7L, "admin", 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateSealUsedByAnotherInbound() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
|
||||
@@ -317,6 +317,21 @@ export async function run(context) {
|
||||
|
||||
const firstPhoto = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_1')
|
||||
const sealEvidence = evidenceItems.find((item) => item.evidenceType === 'SEAL')
|
||||
|
||||
const appendedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_1')
|
||||
const appendedFile = context.apiSuccess(MODULE, '追加同类型第二张装柜证据文件', appendedUpload, '', OPTIONS)
|
||||
const appendedFileId = id(appendedFile?.fileId)
|
||||
const appended = appendedFileId
|
||||
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_1', appendedFileId, suffix, 'PHOTO_STEP_1_APPEND')
|
||||
: appendedUpload
|
||||
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张装柜证据', appended, '', OPTIONS)
|
||||
const appendedItems = appendedData?.evidences?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
|
||||
context.assert(MODULE, '同类型装柜证据支持多图追加且不替换历史', appended,
|
||||
Boolean(appendedFileId) && appendedItems.length >= 2
|
||||
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId))
|
||||
&& appendedItems.some((item) => id(item.fileId) === appendedFileId),
|
||||
`photoStep1Count=${appendedItems.length}, original=${id(firstPhoto?.fileId) || 'missing'}, appended=${appendedFileId || 'missing'}`, OPTIONS)
|
||||
|
||||
const wrongType = await bindEvidence(context, base, tenantId, 'PHOTO_STEP_2', id(firstPhoto?.fileId), suffix, 'wrong-type')
|
||||
context.addStep(MODULE, '不同证据类型的真实 fileId 不可交叉绑定', wrongType, failure(wrongType),
|
||||
`HTTP=${wrongType.http}, code=${codeOf(wrongType)}`, OPTIONS)
|
||||
@@ -328,10 +343,29 @@ export async function run(context) {
|
||||
context.addStep(MODULE, '伪造装柜证据 fileId 归属校验拒绝', forged, failure(forged),
|
||||
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
||||
|
||||
const missingSealText = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
||||
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: id(sealEvidence?.fileId),
|
||||
fileName: `${context.marker}-missing-seal-text.png`, sealNo: ' ', remark: context.marker,
|
||||
}, { 'Idempotency-Key': `${context.marker}-p3-missing-seal-text`.slice(0, 128) })
|
||||
context.addStep(MODULE, '带锁码照片缺少锁码文本时拒绝绑定', missingSealText, failure(missingSealText),
|
||||
`HTTP=${missingSealText.http}, code=${codeOf(missingSealText)}`, OPTIONS)
|
||||
|
||||
const voidTarget = appendedItems.find((item) => id(item.fileId) === appendedFileId)
|
||||
const voidResponse = voidTarget
|
||||
? await request(context, 'POST', `${base}/loading/evidence/${encodeURIComponent(id(voidTarget.id))}/void?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void`.slice(0, 128) })
|
||||
: appended
|
||||
const voidData = context.apiSuccess(MODULE, '作废错误装柜证据并保留审计', voidResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '错图作废后原记录保留且不再计入有效证据', voidResponse,
|
||||
Boolean(voidTarget) && voidData?.evidences?.evidences?.some?.((item) => id(item.id) === id(voidTarget.id)
|
||||
&& (item.status === 'VOID' || item.valid === false) && item.voidReason === `${context.marker}-wrong-photo`),
|
||||
`evidenceId=${id(voidTarget?.id) || 'missing'}, status=${voidTarget?.status || 'missing'}`, OPTIONS)
|
||||
|
||||
const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS)
|
||||
context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList,
|
||||
evidenceListData?.evidences?.length === evidenceTypes.length
|
||||
evidenceListData?.evidences?.length >= evidenceTypes.length
|
||||
&& evidenceTypes.every((type) => evidenceListData.evidences.some((item) => item.evidenceType === type))
|
||||
&& evidenceListData.evidences.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
|
||||
`evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS)
|
||||
@@ -340,7 +374,7 @@ export async function run(context) {
|
||||
{ expectedVersion: loadingVersion, remark: context.marker }, { 'Idempotency-Key': `${context.marker}-p3-complete`.slice(0, 128) })
|
||||
const completed = context.apiSuccess(MODULE, '确认本轮入库单装柜完成', complete, '', OPTIONS)
|
||||
context.assert(MODULE, '装柜完成推进主状态 LOADED 且保留五类证据', complete,
|
||||
completed?.mainStatus === 'LOADED' && completed?.evidences?.evidences?.length === evidenceTypes.length,
|
||||
completed?.mainStatus === 'LOADED' && completed?.evidences?.evidences?.length >= evidenceTypes.length,
|
||||
`mainStatus=${completed?.mainStatus || 'missing'}, evidences=${completed?.evidences?.evidences?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const finalResponse = await context.request('GET', detailPath)
|
||||
@@ -355,4 +389,7 @@ export async function run(context) {
|
||||
pickingTaskId: id(pickingCompleted?.id), marker: context.marker, finalStatus: 'LOADED',
|
||||
}
|
||||
|
||||
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 真机在线限制与设备持久化草稿批量提交',
|
||||
'当前验收运行器只能发起平台 HTTP 请求,未具备 PDA 真机断网、设备草稿隔离及批量提交端点的可执行环境',
|
||||
'device-and-batch-fixture')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user