From dcd079b5b7d7644a7b2f0e4504746ee82ad9a1fa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 18:43:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 14 ++++++- .../PlatformPriceBookController.java | 11 +++++ .../firstleg/domain/PriceBookStatus.java | 1 + .../baiyue/firstleg/dto/PriceBookDtos.java | 11 ++++- .../entity/PriceBookVersionEntity.java | 3 ++ .../repository/PriceBookRepository.java | 11 +++++ .../service/PlatformPriceBookService.java | 22 ++++++++-- .../resources/db/phase2-inbound-schema.sql | 5 ++- .../resources/openapi/fba-pricing-v1.yaml | 17 +++++++- .../controller/PriceBookContractTest.java | 5 ++- .../service/PlatformPriceBookServiceTest.java | 40 ++++++++++++++++++- 11 files changed, 130 insertions(+), 10 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 132ea8880..80d2d6bde 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3872,7 +3872,7 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名', source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID', source_sha256 CHAR(64) NULL COMMENT '原始文件SHA-256', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、VOIDED已作废、FAILED解析失败', blocking_error_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阻断错误数', warning_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '警告数', parse_issues_json JSON NULL COMMENT '上传解析问题及待确认重新校验结果', @@ -3883,6 +3883,9 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( published_by BIGINT UNSIGNED NULL COMMENT '确认并发布价格版本的操作人ID', published_at DATETIME(3) NULL COMMENT '确认并生效时间', superseded_at DATETIME(3) NULL COMMENT '被下一版本替换时间', + voided_by BIGINT UNSIGNED NULL COMMENT '作废操作人ID', + voided_at DATETIME(3) NULL COMMENT '作废时间', + void_reason VARCHAR(500) NULL COMMENT '作废原因', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', @@ -3893,6 +3896,10 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( KEY idx_price_book_source_hash (tenant_id, source_sha256) ) ENGINE=InnoDB COMMENT='平台公共船期价格整表版本'; +CALL add_column_if_missing('fl_price_book_version', 'voided_by', 'BIGINT UNSIGNED NULL COMMENT ''作废操作人ID'' AFTER superseded_at'); +CALL add_column_if_missing('fl_price_book_version', 'voided_at', 'DATETIME(3) NULL COMMENT ''作废时间'' AFTER voided_by'); +CALL add_column_if_missing('fl_price_book_version', 'void_reason', 'VARCHAR(500) NULL COMMENT ''作废原因'' AFTER voided_at'); + CREATE TABLE IF NOT EXISTS fl_price_book_channel ( id BIGINT UNSIGNED NOT NULL COMMENT '版本渠道快照ID', price_book_version_id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID', @@ -14990,8 +14997,11 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'published CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'rate_count', '解析价格数'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'source_file_id', 'File服务原始文件ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'source_sha256', '原始文件SHA-256'); -CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'status', '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'status', '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、VOIDED已作废、FAILED解析失败'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'superseded_at', '被下一版本替换时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'voided_at', '作废时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'voided_by', '作废操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'void_reason', '作废原因'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'tenant_id', '平台公共价格作用域固定为0'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'updated_at', '最后更新时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'updated_by', '最后修改人ID'); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformPriceBookController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformPriceBookController.java index af13b112d..78af65ba8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformPriceBookController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformPriceBookController.java @@ -75,6 +75,17 @@ public class PlatformPriceBookController { return ApiResponse.success(service.publish(versionId, user.userId(), request)); } + @PostMapping("/{versionId}/void") + @PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:edit')") + @BusinessAudit(event = "FBA_PRICE_BOOK_VOID", resourceType = "PRICE_BOOK_VERSION", resourceId = "#versionId", description = "作废待确认船期价格表") + public ApiResponse voidPending( + @AuthenticationPrincipal com.baiyue.common.security.AuthenticatedUser user, + @PathVariable long versionId, + @Valid @RequestBody PriceBookDtos.PriceBookVoidRequest request) { + FirstlegScopeGuard.platform(user); + return ApiResponse.success(service.voidPending(versionId, user.userId(), request)); + } + @GetMapping("/{versionId}/source") @PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')") public ResponseEntity source( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/domain/PriceBookStatus.java b/firstleg/src/main/java/com/baiyue/firstleg/domain/PriceBookStatus.java index 717eefca6..c3bb8a276 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/domain/PriceBookStatus.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/domain/PriceBookStatus.java @@ -5,5 +5,6 @@ public enum PriceBookStatus { PENDING_CONFIRMATION, ACTIVE, SUPERSEDED, + VOIDED, FAILED } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/PriceBookDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/PriceBookDtos.java index f0e0f4317..b4c4943da 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/PriceBookDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/PriceBookDtos.java @@ -38,7 +38,16 @@ public final class PriceBookDtos { LocalDateTime createdAt, Long publishedBy, LocalDateTime publishedAt, - LocalDateTime supersededAt + LocalDateTime supersededAt, + Long voidedBy, + LocalDateTime voidedAt, + String voidReason + ) {} + + public record PriceBookVoidRequest( + @NotBlank(message = "作废原因不能为空") + @Size(max = 500, message = "作废原因不能超过500个字符") + String reason ) {} public record PriceBookVersionDetail( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/PriceBookVersionEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/PriceBookVersionEntity.java index d4fa1f579..69bddf846 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/PriceBookVersionEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/PriceBookVersionEntity.java @@ -28,4 +28,7 @@ public class PriceBookVersionEntity extends BaseEntity { private Long publishedBy; private LocalDateTime publishedAt; private LocalDateTime supersededAt; + private Long voidedBy; + private LocalDateTime voidedAt; + private String voidReason; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/PriceBookRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/PriceBookRepository.java index 7c03889ce..cc744793b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/PriceBookRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/PriceBookRepository.java @@ -156,4 +156,15 @@ public class PriceBookRepository { .set(CarrierFbaRateEntity::getUpdatedBy, operatorId) .set(CarrierFbaRateEntity::getUpdatedAt, at)); } + + public void voidPending(PriceBookVersionEntity value, LocalDateTime at, long operatorId, String reason) { + value.setStatus("VOIDED"); value.setVoidedAt(at); value.setVoidedBy(operatorId); value.setVoidReason(reason); + value.setUpdatedBy(operatorId); value.setUpdatedAt(at); updateVersion(value); + rates.update(null, new LambdaUpdateWrapper() + .eq(CarrierFbaRateEntity::getPriceBookVersionId, value.getId()) + .eq(CarrierFbaRateEntity::getStatus, "PENDING_CONFIRMATION") + .set(CarrierFbaRateEntity::getStatus, "VOIDED") + .set(CarrierFbaRateEntity::getUpdatedBy, operatorId) + .set(CarrierFbaRateEntity::getUpdatedAt, at)); + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java index 9adcf4617..5c1fa8ce2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java @@ -158,6 +158,11 @@ public class PlatformPriceBookService { if (candidate.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE) repository.lockPlatformVersions(); else repository.lockVersions(candidate.getTenantId()); PriceBookVersionEntity version = requirePendingConfirmation(versionId); + List activeVersions = version.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE + ? repository.listActiveForUpdate() : repository.listActiveForUpdate(version.getTenantId()); + if (activeVersions.stream().anyMatch(active -> active.getVersionNo() >= version.getVersionNo())) { + throw new BizException(ResultCode.CONFLICT, "该待确认版本早于当前生效版本,请作废后重新上传"); + } List channels = repository.listChannels(versionId).stream().map(this::channelDto).toList(); List rates = repository.listRates(versionId).stream().map(this::rateDto).toList(); MappingResult mapped = applyMappings(rates, confirmation); @@ -174,14 +179,25 @@ public class PlatformPriceBookService { version.setChannelCount(channels.size()); version.setRateCount(mapped.rates().size()); LocalDateTime now = LocalDateTime.now(); - (version.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE - ? repository.listActiveForUpdate() : repository.listActiveForUpdate(version.getTenantId())).stream() + activeVersions.stream() .filter(active -> !Objects.equals(active.getId(), versionId)) .forEach(active -> repository.supersede(active, now, operatorId)); repository.activate(version, now, operatorId); return detail(versionId); } + @Transactional + public PriceBookDtos.PriceBookVersionDetail voidPending(long versionId, long operatorId, + PriceBookDtos.PriceBookVoidRequest request) { + PriceBookVersionEntity candidate = requireVersion(versionId); + if (candidate.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE) repository.lockPlatformVersions(); + else repository.lockVersions(candidate.getTenantId()); + PriceBookVersionEntity version = requirePendingConfirmation(versionId); + String reason = request.reason().trim(); + repository.voidPending(version, LocalDateTime.now(), operatorId, reason); + return detail(versionId); + } + public ResponseEntity source(long versionId, long operatorId) { PriceBookVersionEntity version = requireVersion(versionId); if (version.getSourceFileId() == null) throw new BizException(ResultCode.NOT_FOUND, "价格表原文件不存在"); @@ -562,7 +578,7 @@ public class PlatformPriceBookService { value.getSourceFileId(), value.getSourceSha256(), status, value.getChannelCount(), value.getRateCount(), value.getBlockingErrorCount(), value.getWarningCount(), value.getParseIssuesJson(), value.getCreatedBy(), value.getCreatedAt(), value.getPublishedBy(), - value.getPublishedAt(), value.getSupersededAt()); + value.getPublishedAt(), value.getSupersededAt(), value.getVoidedBy(), value.getVoidedAt(), value.getVoidReason()); } private static void requireScope(long customerId) { diff --git a/firstleg/src/main/resources/db/phase2-inbound-schema.sql b/firstleg/src/main/resources/db/phase2-inbound-schema.sql index f26279406..9ae7b8d18 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-schema.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-schema.sql @@ -558,7 +558,7 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名', source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID', source_sha256 CHAR(64) NULL COMMENT '原始文件SHA-256', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、VOIDED已作废、FAILED解析失败', blocking_error_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阻断错误数', warning_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '警告数', parse_issues_json JSON NULL COMMENT '上传解析问题及待确认重新校验结果', @@ -569,6 +569,9 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( published_by BIGINT UNSIGNED NULL COMMENT '确认并发布价格版本的操作人ID', published_at DATETIME(3) NULL COMMENT '确认并生效时间', superseded_at DATETIME(3) NULL COMMENT '被下一版本替换时间', + voided_by BIGINT UNSIGNED NULL COMMENT '作废操作人ID', + voided_at DATETIME(3) NULL COMMENT '作废时间', + void_reason VARCHAR(500) NULL COMMENT '作废原因', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index 580aaad9b..8ee870441 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -112,6 +112,13 @@ paths: parameters: [{$ref: '#/components/parameters/VersionId'}] requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/PriceBookConfirmationRequest'}}}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} + /api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/void: + post: + summary: 作废待确认船期价格表版本 + description: 仅允许作废 PENDING_CONFIRMATION 版本;保留版本、原始文件和审计记录,作废后不可再确认生效。需要 firstleg:fba-pricing:rate:edit 权限。 + parameters: [{$ref: '#/components/parameters/VersionId'}] + requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/PriceBookVoidRequest'}}}} + responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/source: get: summary: 下载船期价格表原始文件 @@ -528,7 +535,7 @@ components: effectiveTo: {type: string, format: date-time, nullable: true} PriceBookStatus: type: string - enum: [PENDING_CONFIRMATION, ACTIVE, SUPERSEDED, FAILED] + enum: [PENDING_CONFIRMATION, ACTIVE, SUPERSEDED, VOIDED, FAILED] PriceModel: type: string enum: [FBA_WAREHOUSE_RATE, REGION_WEIGHT_TIER] @@ -564,6 +571,9 @@ components: publishedBy: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} publishedAt: {type: string, format: date-time, nullable: true} supersededAt: {type: string, format: date-time, nullable: true} + voidedBy: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} + voidedAt: {type: string, format: date-time, nullable: true} + voidReason: {type: string, maxLength: 500, nullable: true} PriceBookVersionDetail: type: object required: [version, channels, rates, issues] @@ -600,6 +610,11 @@ components: type: array maxItems: 2 items: {$ref: '#/components/schemas/PriceBookOverseasMapping'} + PriceBookVoidRequest: + type: object + required: [reason] + properties: + reason: {type: string, minLength: 1, maxLength: 500, description: 人工作废原因} PriceBookOriginMapping: type: object required: [originWarehouseCode, warehouseId] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PriceBookContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PriceBookContractTest.java index ae0a6d745..cc32c2703 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PriceBookContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PriceBookContractTest.java @@ -60,7 +60,7 @@ class PriceBookContractTest { void keepsPricingTaxModeSeparate() { assertThat(PriceBookStatus.values()).containsExactly( PriceBookStatus.PENDING_CONFIRMATION, PriceBookStatus.ACTIVE, - PriceBookStatus.SUPERSEDED, PriceBookStatus.FAILED); + PriceBookStatus.SUPERSEDED, PriceBookStatus.VOIDED, PriceBookStatus.FAILED); assertThat(PricingTaxMode.values()).containsExactly(PricingTaxMode.TAX_INCLUDED, PricingTaxMode.TAX_EXCLUDED); assertThat(PricingDestinationType.values()).containsExactly(PricingDestinationType.FBA, PricingDestinationType.OVERSEAS); assertThat(PriceBookDtos.ParsedRate.class.getRecordComponents()) @@ -99,6 +99,7 @@ class PriceBookContractTest { .contains("/api/platform/v1/fba-pricing/carrier-rates/price-books:") .contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}:") .contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/publish:") + .contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/void:") .contains("/api/merchant/v1/first-leg/pricing/channels:") .contains("/api/merchant/v1/first-leg/pricing/estimate:") .contains("PriceBookStatus") @@ -114,6 +115,8 @@ class PriceBookContractTest { .contains("PriceBookParseIssue") .contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/source") .contains("确认并立即生效") + .contains("PriceBookVoidRequest") + .contains("VOIDED") .contains("SUPERSEDED"); String firstlegOpenapi = new ClassPathResource("openapi/firstleg-v1.yaml") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java index 5729b9a57..1e6ff9f5a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java @@ -137,7 +137,30 @@ class PlatformPriceBookServiceTest { assertThat(Arrays.stream(PlatformPriceBookService.class.getDeclaredMethods()) .filter(method -> java.lang.reflect.Modifier.isPublic(method.getModifiers())) .map(java.lang.reflect.Method::getName).toList()) - .containsOnly("list", "upload", "detail", "publish", "source"); + .containsOnly("list", "upload", "detail", "publish", "voidPending", "source"); + } + + @Test + void voidsPendingVersionAndRetainsReasonForAudit() { + PriceBookVersionEntity pending = version(41L, PriceBookStatus.PENDING_CONFIRMATION); + when(repository.findVersion(41L)).thenReturn(pending); + org.mockito.Mockito.doAnswer(invocation -> { + PriceBookVersionEntity value = invocation.getArgument(0); + value.setStatus(PriceBookStatus.VOIDED.name()); + value.setVoidedBy(7L); + value.setVoidReason(invocation.getArgument(3)); + return null; + }).when(repository).voidPending(any(), any(), any(Long.class), any()); + when(repository.listChannels(41L)).thenReturn(List.of()); + when(repository.listRates(41L)).thenReturn(List.of()); + + var result = service.voidPending(41L, 7L, new PriceBookDtos.PriceBookVoidRequest(" 上传错误 ")); + + assertThat(result.version().status()).isEqualTo(PriceBookStatus.VOIDED); + assertThat(result.version().voidedBy()).isEqualTo(7L); + assertThat(result.version().voidReason()).isEqualTo("上传错误"); + verify(repository).lockPlatformVersions(); + verify(repository).voidPending(any(), any(), any(Long.class), org.mockito.ArgumentMatchers.eq("上传错误")); } @Test @@ -284,6 +307,21 @@ class PlatformPriceBookServiceTest { verify(repository, never()).activate(any(), any(), any(Long.class)); } + @Test + void olderPendingVersionCannotReplaceNewerActiveVersion() { + PriceBookVersionEntity pending = version(5L, PriceBookStatus.PENDING_CONFIRMATION); + PriceBookVersionEntity active = version(8L, PriceBookStatus.ACTIVE); + when(repository.findVersion(5L)).thenReturn(pending); + when(repository.listActiveForUpdate()).thenReturn(List.of(active)); + + assertThatThrownBy(() -> service.publish(5L, 7L, confirmation("CN-YIWU-01", 501L))) + .isInstanceOf(BizException.class) + .hasMessageContaining("早于当前生效版本"); + + verify(repository).lockPlatformVersions(); + verify(repository, never()).activate(any(), any(), any(Long.class)); + } + private static PriceBookVersionEntity version(long id, PriceBookStatus status) { PriceBookVersionEntity value = new PriceBookVersionEntity(); value.setId(id);