提交
This commit is contained in:
@@ -3093,7 +3093,7 @@ CREATE TABLE fl_loading_cabinet_evidence (
|
||||
seal_no varchar(64) DEFAULT NULL COMMENT '带锁码照对应锁码',
|
||||
remark varchar(1000) DEFAULT NULL COMMENT '证据备注',
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
||||
active_flag tinyint unsigned DEFAULT '1' COMMENT '有效记录为1,作废记录为NULL,保证每柜每类型仅一条有效事实',
|
||||
active_flag tinyint unsigned DEFAULT '1' COMMENT '有效记录为1,作废记录为NULL,同类允许多张有效照片',
|
||||
operation_key varchar(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||
request_hash char(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||
void_reason varchar(500) DEFAULT NULL COMMENT '作废原因',
|
||||
@@ -3109,7 +3109,7 @@ CREATE TABLE fl_loading_cabinet_evidence (
|
||||
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
||||
UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||
KEY idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||
CONSTRAINT fk_cabinet_evidence_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id),
|
||||
|
||||
+3
@@ -254,6 +254,9 @@ public class ContainerCustomsService {
|
||||
throw conflict("当前柜尚未完成首次发送报关行资料");
|
||||
}
|
||||
CompletionRow completion = mapper.lockCompletion(documentId);
|
||||
if (completion != null && "CONFIRMED".equals(completion.customerConfirmStatus())) {
|
||||
throw conflict("客户已确认预录单,不可再次上传");
|
||||
}
|
||||
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RECEIPT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
||||
long completionId;
|
||||
int version;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public interface CabinetLoadingEvidenceMapper extends BaseMapper<CabinetLoadingE
|
||||
@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")
|
||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} AND evidence_type=#{type} AND active_flag=1 ORDER BY id DESC LIMIT 1 FOR UPDATE")
|
||||
CabinetLoadingEvidenceEntity activeForUpdate(@Param("containerBookingId") long containerBookingId,
|
||||
@Param("type") String type);
|
||||
|
||||
|
||||
+1
-4
@@ -74,8 +74,6 @@ public class CabinetLoadingEvidenceService {
|
||||
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());
|
||||
@@ -84,8 +82,7 @@ public class CabinetLoadingEvidenceService {
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
||||
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
||||
// A replacement may already have voided the old row in this transaction.
|
||||
// Roll it back; a subsequent same-key request can read the committed winner.
|
||||
// The task-scoped operation key protects concurrent duplicate uploads.
|
||||
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
||||
}
|
||||
return view(scope);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Existing databases: allow multiple active photos per evidence type. No photo rows are changed.
|
||||
USE baiyue_firstleg;
|
||||
SET @evidence_ddl = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
||||
AND table_name='fl_loading_cabinet_evidence' AND index_name='uk_cabinet_evidence_active'
|
||||
), 'ALTER TABLE fl_loading_cabinet_evidence DROP INDEX uk_cabinet_evidence_active', 'SELECT 1');
|
||||
PREPARE evidence_ddl FROM @evidence_ddl;
|
||||
EXECUTE evidence_ddl;
|
||||
DEALLOCATE PREPARE evidence_ddl;
|
||||
SET @evidence_ddl = IF(EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
||||
AND table_name='fl_loading_cabinet_evidence' AND index_name='idx_cabinet_evidence_active'
|
||||
), 'SELECT 1', 'ALTER TABLE fl_loading_cabinet_evidence ADD INDEX idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag)');
|
||||
PREPARE evidence_ddl FROM @evidence_ddl;
|
||||
EXECUTE evidence_ddl;
|
||||
DEALLOCATE PREPARE evidence_ddl;
|
||||
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence (
|
||||
seal_no VARCHAR(64) NULL COMMENT '带锁码照对应锁码',
|
||||
remark VARCHAR(1000) NULL COMMENT '证据备注',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
||||
active_flag TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '有效唯一标识;作废后置NULL',
|
||||
active_flag TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '有效标识;同类型允许多张,作废后置NULL',
|
||||
operation_key VARCHAR(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||
void_reason VARCHAR(500) NULL COMMENT '作废原因',
|
||||
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||
KEY idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
||||
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||
|
||||
@@ -140,6 +140,7 @@ paths:
|
||||
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
|
||||
post:
|
||||
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
|
||||
description: 同类型照片追加保留,不替换已有有效照片。expectedVersion取当前同类型最新有效照片版本,无照片时为0;重复幂等键不追加。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
|
||||
+13
@@ -304,6 +304,19 @@ class ContainerCustomsServiceTest {
|
||||
verify(mapper,never()).insertBrokerSubmission(anyLong(),anyLong(),anyLong(),anyString(),anyInt(),anyString(),anyLong(),anyString());
|
||||
}
|
||||
|
||||
@Test void receiptUploadRejectsAfterCustomerConfirmationBeforeBrokerExecution(){
|
||||
var source=document(1L);var declaring=container(ContainerCustomsService.DECLARING,3);
|
||||
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("confirmed"),anyString())).thenReturn(1);
|
||||
when(mapper.document(1L)).thenReturn(source);when(mapper.lockContainerById(2L)).thenReturn(declaring);
|
||||
when(mapper.lockDocument(1L)).thenReturn(source);when(mapper.brokerSubmission(2L)).thenReturn(brokerSubmission());
|
||||
when(mapper.lockCompletion(1L)).thenReturn(new ContainerCustomsMapper.CompletionRow(21L,1L,99L,
|
||||
"receipt.pdf","b".repeat(64),"application/pdf",1L,5L,"admin",java.time.LocalDateTime.now(),1,"CONFIRMED",null));
|
||||
assertThatThrownBy(()->service.uploadCompletion(1L,5L,"admin","confirmed",1,
|
||||
new MockMultipartFile("file","receipt.pdf","application/pdf",new byte[]{1})))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("客户已确认预录单");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@Test void receiptUploadRejectsWhenInitialBrokerSubmissionIsMissing(){
|
||||
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
|
||||
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("before-send"),anyString())).thenReturn(1);
|
||||
|
||||
+5
-5
@@ -58,7 +58,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
||||
}).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE fl_loading_cabinet_evidence(" + columns + ")");
|
||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT uq_operation UNIQUE(source_task_id,operation_key)");
|
||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT uq_seal UNIQUE(seal_no,active_flag)");
|
||||
jdbc.execute("CREATE INDEX idx_active ON fl_loading_cabinet_evidence(container_booking_id,evidence_type,active_flag)");
|
||||
String taskColumns = Arrays.stream(LoadingPickTaskEntity.class.getDeclaredFields())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field ->
|
||||
field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase() + " "
|
||||
@@ -98,7 +98,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacementInsertFailureRollsBackOldActiveRowAndSameRequestCanRetry() {
|
||||
void appendInsertFailureRetainsOldActiveRowAndSameRequestCanRetry() {
|
||||
nextFile = 301; upload("original");
|
||||
var before = snapshot();
|
||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT reject_new_file CHECK(file_id <> 302)");
|
||||
@@ -108,11 +108,11 @@ class CabinetLoadingEvidenceTransactionTest {
|
||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence DROP CONSTRAINT reject_new_file");
|
||||
upload("replacement");
|
||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
||||
.singleElement().satisfies(row -> assertThat(row.getFileId()).isEqualTo(302L));
|
||||
.extracting(CabinetLoadingEvidenceEntity::getFileId).containsExactly(301L, 302L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateKeyAfterStaleReplayReadsMustRollBackPriorVoid() {
|
||||
void duplicateKeyAfterStaleReplayReadsRetainsAllPhotos() {
|
||||
nextFile = 301; upload("original");
|
||||
nextFile = 302; upload("winner");
|
||||
var before = snapshot();
|
||||
@@ -167,7 +167,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
||||
boolean secondPassed = second.get(20, java.util.concurrent.TimeUnit.SECONDS);
|
||||
assertThat(firstPassed || secondPassed).isTrue();
|
||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
||||
.singleElement().satisfies(row -> assertThat(row.getOperationKey()).isEqualTo("concurrent"));
|
||||
.extracting(CabinetLoadingEvidenceEntity::getOperationKey).containsExactly("original", "concurrent");
|
||||
var committed = snapshot();
|
||||
upload("concurrent");
|
||||
assertThat(snapshot()).isEqualTo(committed);
|
||||
|
||||
Reference in New Issue
Block a user