This commit is contained in:
2026-09-11 18:43:57 +08:00
parent 06b9f3eda7
commit dcd079b5b7
11 changed files with 130 additions and 10 deletions
@@ -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]