优化
This commit is contained in:
+3
-3
@@ -70,7 +70,7 @@ public class PlatformContainerController {
|
||||
@GetMapping("/{id}/documents/{documentType}") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<BusinessDocument> logisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.containerLogisticsDocument(id,documentType));}
|
||||
@PostMapping(value="/{id}/documents/{documentType}/upload",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestPart("expectedVersion") @Min(0) int version,@RequestPart(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
|
||||
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestParam("expectedVersion") @Min(0) int version,@RequestParam(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
|
||||
@GetMapping("/{id}/documents/{documentType}/{fileId}/download") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ResponseEntity<byte[]> downloadDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@PathVariable @Positive long fileId){FirstlegScopeGuard.platform(user);var d=loadingLists.downloadContainerDocument(id,documentType,fileId);return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.CONTENT_DISPOSITION,org.springframework.http.ContentDisposition.attachment().filename(d.fileName(),StandardCharsets.UTF_8).build().toString()).contentType(d.contentType()).contentLength(d.content().length).body(d.content());}
|
||||
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:container:create')")
|
||||
@@ -127,8 +127,8 @@ public class PlatformContainerController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
@RequestParam("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestParam(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(loadingLists.uploadContainerClearance(
|
||||
id, user.userId(), user.username(), key, expectedVersion, operationReason, file));
|
||||
|
||||
+2
-2
@@ -129,8 +129,8 @@ public class PlatformLoadingListController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId,
|
||||
@RequestParam @Positive long warehouseId, @RequestHeader("Idempotency-Key") String key,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
@RequestParam("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestParam(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.uploadLoadingDeclaration(planId, warehouseId,
|
||||
user.userId(), user.username(), key, expectedVersion, operationReason, file));
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.List;
|
||||
/** 入库单人工费用明细应用服务。 */
|
||||
@Service
|
||||
public class InboundFeeDetailService {
|
||||
private static final long PLATFORM_TENANT_ID = 0L;
|
||||
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
|
||||
@Resource
|
||||
private FeeItemRepository repository;
|
||||
@@ -59,7 +58,7 @@ public class InboundFeeDetailService {
|
||||
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
|
||||
}
|
||||
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
|
||||
FeeItemEntity feeItem = requiredFreightItem(operatorId, operatorName);
|
||||
FeeItemEntity feeItem = requiredFreightItem();
|
||||
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
|
||||
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
|
||||
|
||||
@@ -240,42 +239,11 @@ public class InboundFeeDetailService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private FeeItemEntity requiredFreightItem(long operatorId, String operatorName) {
|
||||
private FeeItemEntity requiredFreightItem() {
|
||||
FeeItemEntity item = repository.findItemByCode(FREIGHT_FEE_CODE);
|
||||
if (item != null) {
|
||||
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
|
||||
return item;
|
||||
}
|
||||
FeeItemEntity created = new FeeItemEntity();
|
||||
created.setTenantId(PLATFORM_TENANT_ID);
|
||||
created.setCategory("头程运费");
|
||||
created.setTradeScope("INTERNATIONAL");
|
||||
created.setFeeCode(FREIGHT_FEE_CODE);
|
||||
created.setFeeTypeName("FBA头程运费");
|
||||
created.setPickupPayable(false);
|
||||
created.setPickupReceivable(false);
|
||||
created.setWarehousePayable(false);
|
||||
created.setWarehouseReceivable(true);
|
||||
created.setAllocationFee(false);
|
||||
created.setDescription("系统计费发布时自动维护,不允许用于应付费用");
|
||||
created.setStatus("ACTIVE");
|
||||
created.setCreatedBy(operatorId);
|
||||
created.setUpdatedBy(operatorId);
|
||||
created.setCreatedByName(operatorName);
|
||||
created.setUpdatedByName(operatorName);
|
||||
try {
|
||||
repository.insertItem(created);
|
||||
return created;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
FeeItemEntity concurrent = repository.findItemByCode(FREIGHT_FEE_CODE);
|
||||
if (concurrent != null) {
|
||||
if (!"ACTIVE".equals(concurrent.getStatus())) {
|
||||
throw conflict("系统费用项目FBA头程运费已停用");
|
||||
}
|
||||
return concurrent;
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
if (item == null) throw conflict("系统费用项目FBA头程运费未初始化");
|
||||
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
|
||||
return item;
|
||||
}
|
||||
|
||||
private static java.math.BigDecimal effectiveUnitPrice(InboundFeeEntity fee) {
|
||||
|
||||
@@ -463,6 +463,15 @@ CALL phase2_publish_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_publishe
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_index;
|
||||
|
||||
-- 系统计费使用的固定公共费用项目;不属于费用类型.xls 的110条受控种子。
|
||||
INSERT INTO fl_fee_item (id, tenant_id, fee_code, category, trade_scope, fee_type_name,
|
||||
pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, allocation_fee,
|
||||
description, status, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at)
|
||||
SELECT 193000000001000111, 0, 'FBA_HEAD_FREIGHT', '头程运费', 'INTERNATIONAL', 'FBA头程运费',
|
||||
0, 0, 0, 1, 0, '系统计费发布使用的固定应收费用项目', 'ACTIVE',
|
||||
0, 'system', '2026-08-14 00:00:00.000', 0, 'system', '2026-08-14 00:00:00.000'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item WHERE fee_code = 'FBA_HEAD_FREIGHT');
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_schema;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase2_publish_assert_fee_schema()
|
||||
@@ -483,6 +492,10 @@ BEGIN
|
||||
OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND status = 'ACTIVE' AND deleted = 0
|
||||
AND id BETWEEN 193000000001000001 AND 193000000001000110
|
||||
AND fee_code = CONCAT('FEE_TYPE_', LPAD(id - 193000000001000000, 3, '0'))) <> 110
|
||||
OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND fee_code = 'FBA_HEAD_FREIGHT'
|
||||
AND category = '头程运费' AND trade_scope = 'INTERNATIONAL' AND fee_type_name = 'FBA头程运费'
|
||||
AND pickup_payable = 0 AND pickup_receivable = 0 AND warehouse_payable = 0
|
||||
AND warehouse_receivable = 1 AND allocation_fee = 0 AND status = 'ACTIVE' AND deleted = 0) <> 1
|
||||
OR EXISTS (SELECT 1 FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id
|
||||
WHERE d.tenant_id = 0 OR f.tenant_id <> 0) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期迁移失败:费用项目未完成平台公共作用域或明细跨客户引用校验';
|
||||
|
||||
+21
@@ -4,6 +4,8 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -62,6 +64,13 @@ class ContainerControllerContractTest {
|
||||
.doesNotContain("accountId");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("multipart 文本字段使用请求参数绑定")
|
||||
void multipartScalarFieldsUseRequestParameters() {
|
||||
assertMultipartBinding("uploadLogisticsDocument", 4, 5, 6);
|
||||
assertMultipartBinding("uploadClearance", 3, 4, 5);
|
||||
}
|
||||
|
||||
private static void assertPermission(String methodName, String authority) {
|
||||
Method method = Arrays.stream(PlatformContainerController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
@@ -70,4 +79,16 @@ class ContainerControllerContractTest {
|
||||
.isEqualTo("hasAuthority('" + authority + "')");
|
||||
}
|
||||
|
||||
private static void assertMultipartBinding(String methodName, int fileIndex,
|
||||
int versionIndex, int reasonIndex) {
|
||||
Method method = Arrays.stream(PlatformContainerController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(method.getParameters()[fileIndex].getAnnotation(RequestPart.class)).isNotNull();
|
||||
assertThat(method.getParameters()[versionIndex].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[versionIndex].getAnnotation(RequestPart.class)).isNull();
|
||||
assertThat(method.getParameters()[reasonIndex].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[reasonIndex].getAnnotation(RequestPart.class)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
@@ -13,6 +13,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -89,6 +91,18 @@ class LoadingListControllerContractTest {
|
||||
assertThat(component(ConfirmedQuery.class, "warehouseId").getAnnotation(NotNull.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartScalarFieldsUseRequestParameters() throws Exception {
|
||||
Method method = PlatformLoadingListController.class.getDeclaredMethod("uploadDeclaration",
|
||||
AuthenticatedUser.class, long.class, long.class, String.class,
|
||||
MultipartFile.class, int.class, String.class);
|
||||
assertThat(method.getParameters()[4].getAnnotation(RequestPart.class)).isNotNull();
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestPart.class)).isNull();
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestPart.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlParses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
|
||||
|
||||
+3
@@ -22,6 +22,9 @@ class FeeItemMigrationSqlContractTest {
|
||||
.contains("LEFT JOIN tmp_fee_item_seed matched_seed")
|
||||
.contains("matched_seed.fee_code = NULLIF(TRIM(f.fee_code), '')")
|
||||
.contains("matched_seed.trade_scope = f.trade_scope")
|
||||
.contains("193000000001000111, 0, 'FBA_HEAD_FREIGHT'")
|
||||
.contains("fee_code = 'FBA_HEAD_FREIGHT'")
|
||||
.contains("warehouse_receivable = 1")
|
||||
.doesNotContain("LEFT JOIN tmp_fee_item_seed by_code")
|
||||
.doesNotContain("LEFT JOIN tmp_fee_item_seed by_semantic")
|
||||
.doesNotContain("OR (SELECT COUNT(*) FROM tmp_fee_item_seed")
|
||||
|
||||
@@ -183,13 +183,10 @@ class FeeManagementServiceTest {
|
||||
inbound.setCompanyName("客户A有限公司");
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
||||
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
||||
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
FeeItemEntity value = invocation.getArgument(0);
|
||||
assertThat(value.getTenantId()).isZero();
|
||||
value.setId(11L);
|
||||
return null;
|
||||
}).when(repository).insertItem(any());
|
||||
FeeItemEntity freightItem = item(true, false);
|
||||
freightItem.setId(11L);
|
||||
freightItem.setFeeCode("FBA_HEAD_FREIGHT");
|
||||
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(freightItem);
|
||||
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setId(501L);
|
||||
@@ -226,6 +223,26 @@ class FeeManagementServiceTest {
|
||||
assertThat(saved.getSettlementIncluded()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("系统头程运费项目缺失时拒绝运行期补建")
|
||||
void rejectsMissingSystemFreightItem() {
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
||||
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
||||
when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(null);
|
||||
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setId(501L);
|
||||
fee.setInboundId(21L);
|
||||
fee.setFeeStatus("SENT");
|
||||
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
||||
|
||||
assertThatThrownBy(() -> details.publishFreight(
|
||||
10001L, 21L, 501L, 9L, "admin", LocalDateTime.of(2026, 8, 5, 12, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("FBA头程运费未初始化");
|
||||
verify(repository, never()).insertItem(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("并发发布同一计费快照时返回已生成的运费明细")
|
||||
void reusesConcurrentFreightDetail() {
|
||||
|
||||
Reference in New Issue
Block a user