优化
This commit is contained in:
@@ -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');
|
||||
|
||||
+11
@@ -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<PriceBookDtos.PriceBookVersionDetail> 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<byte[]> source(
|
||||
|
||||
@@ -5,5 +5,6 @@ public enum PriceBookStatus {
|
||||
PENDING_CONFIRMATION,
|
||||
ACTIVE,
|
||||
SUPERSEDED,
|
||||
VOIDED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<CarrierFbaRateEntity>()
|
||||
.eq(CarrierFbaRateEntity::getPriceBookVersionId, value.getId())
|
||||
.eq(CarrierFbaRateEntity::getStatus, "PENDING_CONFIRMATION")
|
||||
.set(CarrierFbaRateEntity::getStatus, "VOIDED")
|
||||
.set(CarrierFbaRateEntity::getUpdatedBy, operatorId)
|
||||
.set(CarrierFbaRateEntity::getUpdatedAt, at));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PriceBookVersionEntity> 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<PriceBookDtos.ParsedChannel> channels = repository.listChannels(versionId).stream().map(this::channelDto).toList();
|
||||
List<PriceBookDtos.ParsedRate> 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<byte[]> 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) {
|
||||
|
||||
@@ -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已删除',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
+39
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user