提交
This commit is contained in:
@@ -3093,7 +3093,7 @@ CREATE TABLE fl_loading_cabinet_evidence (
|
|||||||
seal_no varchar(64) DEFAULT NULL COMMENT '带锁码照对应锁码',
|
seal_no varchar(64) DEFAULT NULL COMMENT '带锁码照对应锁码',
|
||||||
remark varchar(1000) DEFAULT NULL COMMENT '证据备注',
|
remark varchar(1000) DEFAULT NULL COMMENT '证据备注',
|
||||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
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 '任务范围内幂等操作键',
|
operation_key varchar(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||||
request_hash char(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
request_hash char(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||||
void_reason varchar(500) DEFAULT NULL COMMENT '作废原因',
|
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 '最后更新时间',
|
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
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_container (container_booking_id,status),
|
||||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||||
CONSTRAINT fk_cabinet_evidence_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id),
|
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("当前柜尚未完成首次发送报关行资料");
|
throw conflict("当前柜尚未完成首次发送报关行资料");
|
||||||
}
|
}
|
||||||
CompletionRow completion = mapper.lockCompletion(documentId);
|
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);
|
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RECEIPT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
||||||
long completionId;
|
long completionId;
|
||||||
int version;
|
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")
|
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} ORDER BY evidence_type,id")
|
||||||
List<CabinetLoadingEvidenceEntity> byContainer(@Param("containerBookingId") long containerBookingId);
|
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,
|
CabinetLoadingEvidenceEntity activeForUpdate(@Param("containerBookingId") long containerBookingId,
|
||||||
@Param("type") String type);
|
@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.getVersion(), expected)) throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||||
if (current != null && Objects.equals(current.getFileId(), request.fileId())
|
if (current != null && Objects.equals(current.getFileId(), request.fileId())
|
||||||
&& Objects.equals(current.getSealNo(), sealNo)) return view(scope);
|
&& 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();
|
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
||||||
value.setContainerBookingId(scope.containerBookingId()); value.setSourceTaskId(taskId);
|
value.setContainerBookingId(scope.containerBookingId()); value.setSourceTaskId(taskId);
|
||||||
value.setEvidenceType(type); value.setFileId(request.fileId()); value.setFileName(file.fileName());
|
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.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
||||||
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
||||||
// A replacement may already have voided the old row in this transaction.
|
// The task-scoped operation key protects concurrent duplicate uploads.
|
||||||
// Roll it back; a subsequent same-key request can read the committed winner.
|
|
||||||
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
||||||
}
|
}
|
||||||
return view(scope);
|
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 '带锁码照对应锁码',
|
seal_no VARCHAR(64) NULL COMMENT '带锁码照对应锁码',
|
||||||
remark VARCHAR(1000) NULL COMMENT '证据备注',
|
remark VARCHAR(1000) NULL COMMENT '证据备注',
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
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 '任务范围内幂等操作键',
|
operation_key VARCHAR(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||||
void_reason VARCHAR(500) NULL COMMENT '作废原因',
|
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 '创建时间',
|
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 '最后更新时间',
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||||
PRIMARY KEY (id),
|
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),
|
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
||||||
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
||||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ paths:
|
|||||||
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
|
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
|
||||||
post:
|
post:
|
||||||
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
|
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
|
||||||
|
description: 同类型照片追加保留,不替换已有有效照片。expectedVersion取当前同类型最新有效照片版本,无照片时为0;重复幂等键不追加。
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/TaskId'}
|
- {$ref: '#/components/parameters/TaskId'}
|
||||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
- {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());
|
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(){
|
@Test void receiptUploadRejectsWhenInitialBrokerSubmissionIsMissing(){
|
||||||
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
|
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);
|
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(","));
|
}).collect(Collectors.joining(","));
|
||||||
jdbc.execute("CREATE TABLE fl_loading_cabinet_evidence(" + columns + ")");
|
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_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())
|
String taskColumns = Arrays.stream(LoadingPickTaskEntity.class.getDeclaredFields())
|
||||||
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field ->
|
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field ->
|
||||||
field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase() + " "
|
field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase() + " "
|
||||||
@@ -98,7 +98,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void replacementInsertFailureRollsBackOldActiveRowAndSameRequestCanRetry() {
|
void appendInsertFailureRetainsOldActiveRowAndSameRequestCanRetry() {
|
||||||
nextFile = 301; upload("original");
|
nextFile = 301; upload("original");
|
||||||
var before = snapshot();
|
var before = snapshot();
|
||||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT reject_new_file CHECK(file_id <> 302)");
|
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");
|
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence DROP CONSTRAINT reject_new_file");
|
||||||
upload("replacement");
|
upload("replacement");
|
||||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
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
|
@Test
|
||||||
void duplicateKeyAfterStaleReplayReadsMustRollBackPriorVoid() {
|
void duplicateKeyAfterStaleReplayReadsRetainsAllPhotos() {
|
||||||
nextFile = 301; upload("original");
|
nextFile = 301; upload("original");
|
||||||
nextFile = 302; upload("winner");
|
nextFile = 302; upload("winner");
|
||||||
var before = snapshot();
|
var before = snapshot();
|
||||||
@@ -167,7 +167,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
boolean secondPassed = second.get(20, java.util.concurrent.TimeUnit.SECONDS);
|
boolean secondPassed = second.get(20, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
assertThat(firstPassed || secondPassed).isTrue();
|
assertThat(firstPassed || secondPassed).isTrue();
|
||||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
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();
|
var committed = snapshot();
|
||||||
upload("concurrent");
|
upload("concurrent");
|
||||||
assertThat(snapshot()).isEqualTo(committed);
|
assertThat(snapshot()).isEqualTo(committed);
|
||||||
|
|||||||
Reference in New Issue
Block a user