优化
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(
|
||||
|
||||
Reference in New Issue
Block a user