feat(firstleg): model declaration batches and customs results
This commit is contained in:
+7
@@ -33,6 +33,13 @@ public class MerchantContainerCustomsController {
|
||||
return ApiResponse.success(service.merchantOrderView(FirstlegScopeGuard.merchant(user), orderId));
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{orderId}/clearance-results")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
public ApiResponse<List<ClearanceResultView>> clearanceResults(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long orderId) {
|
||||
return ApiResponse.success(service.merchantClearanceResults(FirstlegScopeGuard.merchant(user), orderId));
|
||||
}
|
||||
|
||||
@PostMapping(value="/loading-plans/{planId}/documents/upload",consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('merchant:order:edit')")
|
||||
public ApiResponse<DocumentView> upload(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+61
@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@@ -33,6 +34,66 @@ public class PlatformContainerCustomsController {
|
||||
return ApiResponse.success(service.platformView(planId));
|
||||
}
|
||||
|
||||
@GetMapping("/{planId}/declaration-batches")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:list')")
|
||||
public ApiResponse<List<DeclarationBatchView>> declarationBatches(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.platformDeclarationBatches(planId));
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/declaration-batches")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_DECLARATION_BATCH_CREATE",resourceType="CUSTOMS_CONTAINER")
|
||||
public ApiResponse<DeclarationBatchView> createDeclarationBatch(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody DeclarationBatchRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.createDeclarationBatch(planId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@PostMapping("/declaration-batches/{batchId}/submit")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_DECLARATION_BATCH_SUBMIT",resourceType="CUSTOMS_DECLARATION_BATCH")
|
||||
public ApiResponse<DeclarationBatchView> submitDeclarationBatch(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long batchId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody DeclarationBatchRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.submitDeclarationBatch(batchId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@GetMapping("/declaration-batches/{batchId}/results")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:list')")
|
||||
public ApiResponse<List<ClearanceResultView>> clearanceResults(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long batchId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.platformClearanceResults(batchId));
|
||||
}
|
||||
|
||||
@PostMapping("/declaration-batches/{batchId}/results")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_CLEARANCE_RESULT_RECORD",resourceType="CUSTOMS_DECLARATION_BATCH")
|
||||
public ApiResponse<ClearanceResultView> addClearanceResult(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long batchId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ClearanceResultRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.addClearanceResult(batchId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@PostMapping("/clearance-results/{resultId}/resolve")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_CLEARANCE_RESULT_RESOLVE",resourceType="CUSTOMS_CLEARANCE_RESULT")
|
||||
public ApiResponse<ClearanceResultView> resolveClearanceResult(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long resultId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ClearanceResolveRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.resolveClearanceResult(resultId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/start")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CONTAINER_CUSTOMS_START",resourceType="CUSTOMS_CONTAINER")
|
||||
|
||||
+25
@@ -35,5 +35,30 @@ public final class ContainerCustomsDtos {
|
||||
public record ActionRequest(@Min(0) int expectedVersion, boolean confirm,
|
||||
@Size(max = 500) String remark) {}
|
||||
|
||||
public record DeclarationBatchView(long id, long customsContainerId, long loadingPlanId,
|
||||
String batchNo, String status, int sourceVersion,
|
||||
String customsEntryNo, long version,
|
||||
LocalDateTime submittedAt, String submittedByName,
|
||||
LocalDateTime acknowledgedAt, String acknowledgedByName,
|
||||
String remark, List<ClearanceResultView> results) {}
|
||||
|
||||
public record DeclarationBatchRequest(@Min(0) int expectedVersion,
|
||||
@Size(max = 64) String batchNo,
|
||||
@Size(max = 64) String customsEntryNo,
|
||||
@Size(max = 500) String remark) {}
|
||||
|
||||
public record ClearanceResultView(long id, long declarationBatchId, long customsContainerId,
|
||||
String resultType, String resultStatus, String customsEntryNo,
|
||||
LocalDateTime occurredAt, String resolution, Long evidenceFileId,
|
||||
long recordedBy, String recordedByName, LocalDateTime recordedAt,
|
||||
Long previousResultId) {}
|
||||
|
||||
public record ClearanceResultRequest(@Min(0) int expectedVersion, @Size(max = 32) String resultType,
|
||||
LocalDateTime occurredAt, @Size(max = 1000) String resolution,
|
||||
Long evidenceFileId, boolean resolved) {}
|
||||
|
||||
public record ClearanceResolveRequest(@Min(0) int expectedVersion,
|
||||
@Size(max = 1000) String resolution) {}
|
||||
|
||||
public record Download(String fileName, String contentType, byte[] content) {}
|
||||
}
|
||||
|
||||
+102
@@ -119,6 +119,94 @@ public interface ContainerCustomsMapper {
|
||||
@Select("SELECT broker_submitted_at FROM fl_customs_container WHERE id=#{id}")
|
||||
LocalDateTime brokerSubmittedAt(@Param("id") long id);
|
||||
|
||||
@Select("SELECT " + DECLARATION_BATCH_COLUMNS + " FROM fl_customs_declaration_batch b "
|
||||
+ "WHERE customs_container_id=#{containerId} ORDER BY created_at DESC,id DESC")
|
||||
List<DeclarationBatchRow> declarationBatches(@Param("containerId") long containerId);
|
||||
|
||||
@Select("SELECT " + DECLARATION_BATCH_COLUMNS + " FROM fl_customs_declaration_batch b WHERE b.id=#{id}")
|
||||
DeclarationBatchRow declarationBatch(@Param("id") long id);
|
||||
|
||||
@Select("SELECT " + DECLARATION_BATCH_COLUMNS + " FROM fl_customs_declaration_batch b WHERE b.id=#{id} FOR UPDATE")
|
||||
DeclarationBatchRow lockDeclarationBatch(@Param("id") long id);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_declaration_batch
|
||||
(id,customs_container_id,loading_plan_id,batch_no,status,source_version,source_snapshot_json,remark,
|
||||
version,created_at,updated_at)
|
||||
VALUES(#{id},#{containerId},#{planId},#{batchNo},'READY',#{sourceVersion},CAST(#{snapshotJson} AS JSON),#{remark},
|
||||
0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertDeclarationBatch(@Param("id") long id,@Param("containerId") long containerId,@Param("planId") long planId,
|
||||
@Param("batchNo") String batchNo,@Param("sourceVersion") int sourceVersion,
|
||||
@Param("snapshotJson") String snapshotJson,@Param("remark") String remark);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_declaration_batch SET batch_no=#{batchNo},status='SUBMITTED',customs_entry_no=#{entryNo},
|
||||
submitted_by=#{operatorId},submitted_by_name=#{operatorName},submitted_at=CURRENT_TIMESTAMP(3),remark=#{remark},
|
||||
version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND version=#{version} AND status='READY'
|
||||
""")
|
||||
int submitDeclarationBatch(@Param("id") long id,@Param("version") int version,@Param("batchNo") String batchNo,
|
||||
@Param("entryNo") String entryNo,@Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName,@Param("remark") String remark);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_declaration_batch SET status=#{status},acknowledged_by=#{operatorId},
|
||||
acknowledged_by_name=#{operatorName},acknowledged_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND version=#{version} AND status IN ('SUBMITTED','RESULT_PENDING','ACKNOWLEDGED')
|
||||
""")
|
||||
int acknowledgeDeclarationBatch(@Param("id") long id,@Param("version") int version,@Param("status") String status,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("SELECT " + CLEARANCE_RESULT_COLUMNS + " FROM fl_customs_clearance_result r "
|
||||
+ "WHERE declaration_batch_id=#{batchId} ORDER BY occurred_at,id")
|
||||
List<ClearanceResultRow> clearanceResults(@Param("batchId") long batchId);
|
||||
|
||||
@Select("SELECT " + CLEARANCE_RESULT_COLUMNS + " FROM fl_customs_clearance_result r WHERE r.id=#{id}")
|
||||
ClearanceResultRow clearanceResult(@Param("id") long id);
|
||||
|
||||
@Select("SELECT " + CLEARANCE_RESULT_COLUMNS + " FROM fl_customs_clearance_result r WHERE r.id=#{id} FOR UPDATE")
|
||||
ClearanceResultRow lockClearanceResult(@Param("id") long id);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_clearance_result
|
||||
(id,declaration_batch_id,customs_container_id,result_type,result_status,customs_entry_no,occurred_at,
|
||||
resolution,evidence_file_id,recorded_by,recorded_by_name,recorded_at,previous_result_id,created_at,updated_at)
|
||||
VALUES(#{id},#{batchId},#{containerId},#{resultType},#{resultStatus},#{entryNo},#{occurredAt},#{resolution},
|
||||
#{evidenceFileId},#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),#{previousResultId},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertClearanceResult(@Param("id") long id,@Param("batchId") long batchId,@Param("containerId") long containerId,
|
||||
@Param("resultType") String resultType,@Param("resultStatus") String resultStatus,
|
||||
@Param("entryNo") String entryNo,@Param("occurredAt") LocalDateTime occurredAt,
|
||||
@Param("resolution") String resolution,@Param("evidenceFileId") Long evidenceFileId,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName,
|
||||
@Param("previousResultId") Long previousResultId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_clearance_result SET result_status='RESOLVED',resolution=#{resolution},updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND result_status='OPEN'
|
||||
""")
|
||||
int resolveClearanceResult(@Param("id") long id,@Param("resolution") String resolution);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM fl_customs_clearance_result WHERE declaration_batch_id=#{batchId} AND result_status='OPEN'")
|
||||
int openClearanceResultCount(@Param("batchId") long batchId);
|
||||
|
||||
@Update("UPDATE fl_customs_declaration_batch SET status='CLOSED',version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status IN ('SUBMITTED','ACKNOWLEDGED','RESULT_PENDING')")
|
||||
int closeDeclarationBatch(@Param("id") long id);
|
||||
|
||||
@Update("UPDATE fl_customs_declaration_batch SET status='RESULT_PENDING',version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status IN ('SUBMITTED','ACKNOWLEDGED')")
|
||||
int markDeclarationResultPending(@Param("id") long id);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT b.id
|
||||
FROM fl_customs_declaration_batch b
|
||||
JOIN fl_customs_clearance_result r ON r.declaration_batch_id=b.id
|
||||
JOIN fl_customs_customer_document d ON d.customs_container_id=b.customs_container_id AND d.tenant_id=#{tenantId} AND d.status='ACTIVE'
|
||||
JOIN fl_customs_frozen_document_inbound s ON s.document_id=d.id AND s.order_id=#{orderId}
|
||||
ORDER BY b.id DESC
|
||||
""")
|
||||
List<Long> merchantDeclarationBatchIds(@Param("tenantId") long tenantId,@Param("orderId") long orderId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_container SET status='COMPLETED',completed_by=#{operatorId},completed_by_name=#{operatorName},
|
||||
completed_at=CURRENT_TIMESTAMP(3),updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
@@ -469,6 +557,20 @@ public interface ContainerCustomsMapper {
|
||||
Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version) {}
|
||||
record CommandRow(Long id,Long actorId,String action,String idempotencyKey,String requestHash,String status,
|
||||
Long resultId,LocalDateTime createdAt,LocalDateTime completedAt) {}
|
||||
String DECLARATION_BATCH_COLUMNS = "b.id id,b.customs_container_id customsContainerId,b.loading_plan_id loadingPlanId,"+
|
||||
"b.batch_no batchNo,b.status status,b.source_version sourceVersion,b.customs_entry_no customsEntryNo,b.submitted_by_name submittedByName,"+
|
||||
"b.submitted_at submittedAt,b.acknowledged_by_name acknowledgedByName,b.acknowledged_at acknowledgedAt,b.remark remark,b.version version,b.created_at createdAt,b.updated_at updatedAt";
|
||||
String CLEARANCE_RESULT_COLUMNS = "r.id id,r.declaration_batch_id declarationBatchId,r.customs_container_id customsContainerId,"+
|
||||
"r.result_type resultType,r.result_status resultStatus,r.customs_entry_no customsEntryNo,r.occurred_at occurredAt,"+
|
||||
"r.resolution resolution,r.evidence_file_id evidenceFileId,r.recorded_by recordedBy,r.recorded_by_name recordedByName,"+
|
||||
"r.recorded_at recordedAt,r.previous_result_id previousResultId";
|
||||
record DeclarationBatchRow(Long id,Long customsContainerId,Long loadingPlanId,String batchNo,String status,
|
||||
Integer sourceVersion,String customsEntryNo,String submittedByName,LocalDateTime submittedAt,
|
||||
String acknowledgedByName,LocalDateTime acknowledgedAt,String remark,Integer version,
|
||||
LocalDateTime createdAt,LocalDateTime updatedAt) {}
|
||||
record ClearanceResultRow(Long id,Long declarationBatchId,Long customsContainerId,String resultType,String resultStatus,
|
||||
String customsEntryNo,LocalDateTime occurredAt,String resolution,Long evidenceFileId,
|
||||
Long recordedBy,String recordedByName,LocalDateTime recordedAt,Long previousResultId) {}
|
||||
record BuyExportRow(Long itemId,String planNo,String cabinetNo,String containerNo,String customerName,String orderNo,
|
||||
String inboundCode,String skuCode,String productNameZh,String productNameEn,Integer packageCount,
|
||||
Integer productQuantity,BigDecimal grossWeightKg,BigDecimal netWeightKg,BigDecimal volumeCbm,
|
||||
|
||||
+143
@@ -309,6 +309,149 @@ public class ContainerCustomsService {
|
||||
return platformView(planId);
|
||||
}
|
||||
|
||||
public List<DeclarationBatchView> platformDeclarationBatches(long planId) {
|
||||
ContainerRow container = mapper.container(planId);
|
||||
if (container == null) throw notFound("整柜报关尚未生成");
|
||||
return mapper.declarationBatches(container.id()).stream().map(this::declarationBatchView).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeclarationBatchView createDeclarationBatch(long planId, long operatorId, String operatorName,
|
||||
String key, DeclarationBatchRequest request) {
|
||||
String action = "CREATE_CUSTOMS_DECLARATION_BATCH_" + planId;
|
||||
String requestHash = hash(action, request.expectedVersion(), request.batchNo(), request.remark());
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return declarationBatchView(mapper.declarationBatch(replay.resultId()));
|
||||
ContainerRow container = mapper.lockContainer(planId);
|
||||
if (container == null || !COMPLETED.equals(container.status())) throw conflict("报关完税文件未全部完成,不能创建正式申报批次");
|
||||
if (mapper.taxRefundCount(planId) == 0) throw conflict("全买单柜不创建退税正式申报批次");
|
||||
if (mapper.missingFinalCount(container.id()) != 0 || mapper.unconfirmedReceiptCount(container.id()) != 0
|
||||
|| mapper.brokerSubmittedAt(container.id()) == null) {
|
||||
throw conflict("当前柜尚未完成退税资料、回执确认和报关行执行");
|
||||
}
|
||||
if (!Objects.equals(container.version(), request.expectedVersion())) throw concurrent();
|
||||
long batchId = IdWorker.getId();
|
||||
String batchNo = trim(request.batchNo());
|
||||
if (batchNo == null) batchNo = "DECL-" + batchId;
|
||||
String snapshot = declarationSnapshot(container.id());
|
||||
if (mapper.insertDeclarationBatch(batchId, container.id(), planId, batchNo, container.version(), snapshot,
|
||||
trim(request.remark())) != 1) throw conflict("正式申报批次创建失败");
|
||||
history(container, "CREATE_CUSTOMS_DECLARATION_BATCH", container.status(), container.status(), batchNo, operatorId, operatorName);
|
||||
completeCommand(operatorId, action, key, batchId);
|
||||
return declarationBatchView(mapper.declarationBatch(batchId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeclarationBatchView submitDeclarationBatch(long batchId, long operatorId, String operatorName,
|
||||
String key, DeclarationBatchRequest request) {
|
||||
String action = "SUBMIT_CUSTOMS_DECLARATION_BATCH_" + batchId;
|
||||
String entryNo = trim(request.customsEntryNo());
|
||||
if (entryNo == null) throw bad("正式申报必须回填报关单号");
|
||||
String requestHash = hash(action, request.expectedVersion(), request.batchNo(), entryNo, request.remark());
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return declarationBatchView(mapper.declarationBatch(batchId));
|
||||
DeclarationBatchRow batch = mapper.lockDeclarationBatch(batchId);
|
||||
if (batch == null || !"READY".equals(batch.status())) throw conflict("正式申报批次不在可提交状态");
|
||||
if (!Objects.equals(batch.version(), request.expectedVersion())) throw concurrent();
|
||||
String batchNo = trim(request.batchNo());
|
||||
if (batchNo == null) batchNo = batch.batchNo();
|
||||
if (mapper.submitDeclarationBatch(batchId, batch.version(), batchNo, entryNo, operatorId, operatorName,
|
||||
trim(request.remark())) != 1) throw concurrent();
|
||||
ContainerRow container = mapper.lockContainerById(batch.customsContainerId());
|
||||
if (container != null) history(container, "SUBMIT_CUSTOMS_DECLARATION_BATCH", container.status(), container.status(), entryNo, operatorId, operatorName);
|
||||
completeCommand(operatorId, action, key, batchId);
|
||||
return declarationBatchView(mapper.declarationBatch(batchId));
|
||||
}
|
||||
|
||||
public List<ClearanceResultView> platformClearanceResults(long batchId) {
|
||||
if (mapper.declarationBatch(batchId) == null) throw notFound("正式申报批次不存在");
|
||||
return mapper.clearanceResults(batchId).stream().map(this::clearanceResultView).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ClearanceResultView addClearanceResult(long batchId, long operatorId, String operatorName,
|
||||
String key, ClearanceResultRequest request) {
|
||||
String type = trim(request.resultType());
|
||||
if (!Set.of("INSPECTION", "SUPPLEMENT_REQUIRED", "RELEASED", "DETAINED", "OTHER").contains(type)) {
|
||||
throw bad("海关结果类型无效");
|
||||
}
|
||||
if (request.occurredAt() == null) throw bad("海关结果发生时间不能为空");
|
||||
if (Set.of("DETAINED", "OTHER", "SUPPLEMENT_REQUIRED").contains(type)
|
||||
&& (request.resolution() == null || request.resolution().isBlank())) throw bad("该海关结果必须填写处理说明");
|
||||
String action = "ADD_CUSTOMS_CLEARANCE_RESULT_" + batchId;
|
||||
String requestHash = hash(action, request.expectedVersion(), type, request.occurredAt(), request.resolution(),
|
||||
request.evidenceFileId(), request.resolved());
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return clearanceResultView(mapper.clearanceResult(replay.resultId()));
|
||||
DeclarationBatchRow batch = mapper.lockDeclarationBatch(batchId);
|
||||
if (batch == null || !Set.of("SUBMITTED", "ACKNOWLEDGED", "RESULT_PENDING").contains(batch.status())) {
|
||||
throw conflict("正式申报批次不在可录入海关结果状态");
|
||||
}
|
||||
if (!Objects.equals(batch.version(), request.expectedVersion())) throw concurrent();
|
||||
String resultStatus = "RELEASED".equals(type) || request.resolved() ? "RESOLVED" : "OPEN";
|
||||
long resultId = IdWorker.getId();
|
||||
if (mapper.insertClearanceResult(resultId, batch.id(), batch.customsContainerId(), type, resultStatus,
|
||||
batch.customsEntryNo(), request.occurredAt(), trim(request.resolution()), request.evidenceFileId(),
|
||||
operatorId, operatorName, null) != 1) throw conflict("海关结果保存失败");
|
||||
if ("OPEN".equals(resultStatus)) mapper.markDeclarationResultPending(batch.id());
|
||||
else mapper.closeDeclarationBatch(batch.id());
|
||||
ContainerRow container = mapper.lockContainerById(batch.customsContainerId());
|
||||
if (container != null) history(container, "RECORD_CUSTOMS_CLEARANCE_RESULT", container.status(), container.status(), type, operatorId, operatorName);
|
||||
completeCommand(operatorId, action, key, resultId);
|
||||
return clearanceResultView(mapper.clearanceResult(resultId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ClearanceResultView resolveClearanceResult(long resultId, long operatorId, String operatorName,
|
||||
String key, ClearanceResolveRequest request) {
|
||||
String action = "RESOLVE_CUSTOMS_CLEARANCE_RESULT_" + resultId;
|
||||
String requestHash = hash(action, request.expectedVersion(), request.resolution());
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return clearanceResultView(mapper.clearanceResult(resultId));
|
||||
ClearanceResultRow result = mapper.lockClearanceResult(resultId);
|
||||
if (result == null) throw notFound("海关结果不存在");
|
||||
DeclarationBatchRow batch = mapper.lockDeclarationBatch(result.declarationBatchId());
|
||||
if (batch == null || !Objects.equals(batch.version(), request.expectedVersion())) throw concurrent();
|
||||
if (mapper.resolveClearanceResult(resultId, trim(request.resolution())) != 1) throw conflict("海关结果已被其他人处理");
|
||||
if (mapper.openClearanceResultCount(batch.id()) == 0) mapper.closeDeclarationBatch(batch.id());
|
||||
ContainerRow container = mapper.lockContainerById(batch.customsContainerId());
|
||||
if (container != null) history(container, "RESOLVE_CUSTOMS_CLEARANCE_RESULT", container.status(), container.status(), trim(request.resolution()), operatorId, operatorName);
|
||||
completeCommand(operatorId, action, key, resultId);
|
||||
return clearanceResultView(mapper.clearanceResult(resultId));
|
||||
}
|
||||
|
||||
public List<ClearanceResultView> merchantClearanceResults(long tenantId, long orderId) {
|
||||
return mapper.merchantDeclarationBatchIds(tenantId, orderId).stream()
|
||||
.flatMap(id -> mapper.clearanceResults(id).stream()).map(this::clearanceResultView).toList();
|
||||
}
|
||||
|
||||
private DeclarationBatchView declarationBatchView(DeclarationBatchRow row) {
|
||||
if (row == null) throw notFound("正式申报批次不存在");
|
||||
return new DeclarationBatchView(row.id(), row.customsContainerId(), row.loadingPlanId(), row.batchNo(), row.status(),
|
||||
row.sourceVersion() == null ? 0 : row.sourceVersion(), row.customsEntryNo(), row.version() == null ? 0 : row.version(),
|
||||
row.submittedAt(), row.submittedByName(), row.acknowledgedAt(), row.acknowledgedByName(), row.remark(),
|
||||
mapper.clearanceResults(row.id()).stream().map(this::clearanceResultView).toList());
|
||||
}
|
||||
|
||||
private ClearanceResultView clearanceResultView(ClearanceResultRow row) {
|
||||
if (row == null) throw notFound("海关结果不存在");
|
||||
return new ClearanceResultView(row.id(), row.declarationBatchId(), row.customsContainerId(), row.resultType(),
|
||||
row.resultStatus(), row.customsEntryNo(), row.occurredAt(), row.resolution(), row.evidenceFileId(),
|
||||
row.recordedBy() == null ? 0 : row.recordedBy(), row.recordedByName(), row.recordedAt(), row.previousResultId());
|
||||
}
|
||||
|
||||
private String declarationSnapshot(long containerId) {
|
||||
List<Map<String,Object>> snapshot = mapper.documents(containerId, null).stream().map(doc -> {
|
||||
CompletionRow receipt = mapper.completion(doc.id());
|
||||
FinalRow finalDocument = mapper.finalDocument(doc.id());
|
||||
return Map.<String,Object>of("documentId", doc.id(), "tenantId", doc.tenantId(), "sourceVersion", doc.version(),
|
||||
"receiptVersion", receipt == null ? 0 : receipt.version(), "receiptStatus", receipt == null ? "MISSING" : receipt.customerConfirmStatus(),
|
||||
"finalVersion", finalDocument == null ? 0 : finalDocument.version());
|
||||
}).toList();
|
||||
try { return objectMapper.writeValueAsString(snapshot); }
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException(ex); }
|
||||
}
|
||||
|
||||
public Download downloadSource(long tenantId, long documentId, boolean platform) {
|
||||
DocumentRow source = requiredDocument(documentId);
|
||||
if (!platform && source.tenantId() != tenantId) throw notFound("客户报送资料不存在");
|
||||
|
||||
Reference in New Issue
Block a user