优化
This commit is contained in:
@@ -3189,8 +3189,12 @@ CREATE TABLE IF NOT EXISTS fl_price_book_channel (
|
||||
reference_transit VARCHAR(128) NULL COMMENT '参考时效',
|
||||
surcharge_description VARCHAR(2000) NULL COMMENT '附加费说明,仅展示',
|
||||
remark VARCHAR(2000) NULL COMMENT '渠道备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_book_channel (price_book_version_id, channel_code),
|
||||
KEY idx_price_book_channel_code (channel_code, price_book_version_id)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** Print Service contract for stateless inbound carton label rendering. */
|
||||
@FeignClient(name = "print-service", contextId = "firstlegPrintPdfClient",
|
||||
url = "${baiyue.clients.print.url:}", path = "/internal/print/v1/pdfs")
|
||||
public interface PrintPdfClient {
|
||||
@PostMapping(value = "/inbound-marks", consumes = "application/json")
|
||||
ResponseEntity<byte[]> generateInboundMarks(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
|
||||
@RequestBody InboundMarkPdfRequest request);
|
||||
|
||||
record InboundMarkPdfRequest(String templateCode, PageSpec pageSpec,
|
||||
String downloadFileName, List<Label> labels) {
|
||||
}
|
||||
|
||||
record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {
|
||||
}
|
||||
|
||||
record Label(String topCode, String warehouseText, String cartonText,
|
||||
String routeText, String qrContent) {
|
||||
}
|
||||
}
|
||||
+24
@@ -11,6 +11,7 @@ import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.InboundCartonLabelPrintService;
|
||||
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -22,9 +23,14 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 平台端入库单列表和详情只读接口。 */
|
||||
@Tag(name = "平台入库单", description = "二期入库单列表、页签、筛选和只读聚合详情")
|
||||
@@ -33,6 +39,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/inbounds")
|
||||
public class PlatformInboundOrderController {
|
||||
@Resource private InboundOrderQueryService service;
|
||||
@Resource private InboundCartonLabelPrintService cartonLabelPrintService;
|
||||
|
||||
@Operation(summary = "分页查询入库单",
|
||||
description = "默认分页查询全部企业入库单;可按目标租户和二期状态页签筛选入库单快照。")
|
||||
@@ -61,6 +68,23 @@ public class PlatformInboundOrderController {
|
||||
return ApiResponse.success(service.detail(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "生成入仓箱唛 PDF",
|
||||
description = "首次调用按订单货物明细冻结整单箱号;重复打印及指定箱下标补打复用原箱号映射。")
|
||||
@PostMapping("/{inboundId}/carton-label-pdf")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:view')")
|
||||
public ResponseEntity<byte[]> printCartonLabels(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestParam(required = false) List<@Min(1) Integer> cartonIndex,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
if (inboundId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "入库单ID必须大于零");
|
||||
}
|
||||
return cartonLabelPrintService.generate(tenantId, inboundId, cartonIndex, requestId);
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class InboundMeasurementResponses {
|
||||
String originalDocumentNo,
|
||||
BigDecimal originalGrossWeightKg,
|
||||
BigDecimal originalVolumeCbm,
|
||||
Integer originalPackageCount,
|
||||
Integer actualPackageCount,
|
||||
BigDecimal actualGrossWeightKg,
|
||||
BigDecimal actualVolumeCbm,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
||||
public interface PriceBookChannelMapper extends BaseMapper<PriceBookChannelEntity> {}
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface PriceBookChannelMapper extends BaseMapper<PriceBookChannelEntity> {
|
||||
@Delete("DELETE FROM fl_price_book_channel WHERE price_book_version_id = #{versionId}")
|
||||
int physicallyDeleteByPriceBookVersionId(@Param("versionId") long versionId);
|
||||
}
|
||||
|
||||
+7
@@ -42,6 +42,13 @@ public class InboundMeasurementRepository {
|
||||
.eq(InboundOrderEntity::getId, inboundId));
|
||||
}
|
||||
|
||||
public InboundOrderEntity lockInbound(long tenantId, long inboundId) {
|
||||
return inboundOrders.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, tenantId)
|
||||
.eq(InboundOrderEntity::getId, inboundId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public boolean updateInbound(InboundOrderEntity inbound) {
|
||||
return inboundOrders.update(inbound, new LambdaUpdateWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, inbound.getTenantId())
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundIdempotencyEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
@@ -16,6 +17,7 @@ import com.baiyue.firstleg.mapper.InboundAllocationQueryMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundIdempotencyMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundMeasurementMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundMeasurementItemMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
@@ -30,6 +32,8 @@ public class InboundWorkflowRepository {
|
||||
@Resource
|
||||
private InboundMeasurementMapper measurements;
|
||||
@Resource
|
||||
private InboundMeasurementItemMapper measurementItems;
|
||||
@Resource
|
||||
private MeasureReviewRecordMapper reviews;
|
||||
@Resource
|
||||
private InboundStatusHistoryMapper histories;
|
||||
@@ -55,6 +59,13 @@ public class InboundWorkflowRepository {
|
||||
.eq(InboundMeasurementEntity::getId, measurementId));
|
||||
}
|
||||
|
||||
public java.util.List<InboundMeasurementItemEntity> listMeasurementItems(
|
||||
long tenantId, long measurementId) {
|
||||
return measurementItems.selectList(new LambdaQueryWrapper<InboundMeasurementItemEntity>()
|
||||
.eq(InboundMeasurementItemEntity::getTenantId, tenantId)
|
||||
.eq(InboundMeasurementItemEntity::getMeasurementId, measurementId));
|
||||
}
|
||||
|
||||
public int transitionReview(long tenantId, long inboundId, long operatorId, int expectedVersion,
|
||||
boolean approved) {
|
||||
return approved
|
||||
|
||||
@@ -99,7 +99,7 @@ public class PriceBookRepository {
|
||||
}
|
||||
public void deletePendingChildren(long versionId) {
|
||||
rates.physicallyDeletePendingByPriceBookVersionId(versionId);
|
||||
channels.delete(new LambdaQueryWrapper<PriceBookChannelEntity>().eq(PriceBookChannelEntity::getPriceBookVersionId, versionId));
|
||||
channels.physicallyDeleteByPriceBookVersionId(versionId);
|
||||
}
|
||||
public List<PriceChannelCatalogEntity> activeCatalog() {
|
||||
return catalog.selectList(new LambdaQueryWrapper<PriceChannelCatalogEntity>()
|
||||
|
||||
@@ -68,7 +68,7 @@ public class CartonMeasurementService {
|
||||
UpsertOutcome outcome;
|
||||
try {
|
||||
outcome = transactionTemplate.execute(status -> persist(
|
||||
tenantId, operatorId, binding, inbound, boxCode, command, uploaded.fileId()));
|
||||
tenantId, operatorId, binding, boxCode, command, uploaded.fileId()));
|
||||
} catch (RuntimeException exception) {
|
||||
cleanupImage(tenantId, operatorId, inbound.getId(), uploaded.fileId(), "回传落库失败补偿");
|
||||
throw exception;
|
||||
@@ -111,9 +111,8 @@ public class CartonMeasurementService {
|
||||
if (!repository.updateMeasurement(measurement)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
// 提交审核后仍允许平台人工修正代表箱数,入库单快照必须随当前批次同步。
|
||||
// 提交审核后仍允许平台人工修正代表箱数,但订单原始总箱数不可被测量汇总覆盖。
|
||||
if ("MEASURE_PENDING".equals(inbound.getProcessStatus())) {
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
if (!repository.updateInbound(inbound)) {
|
||||
@@ -125,8 +124,15 @@ public class CartonMeasurementService {
|
||||
|
||||
private UpsertOutcome persist(long tenantId, long operatorId,
|
||||
CartonLabelBindingResolver.Binding binding,
|
||||
InboundOrderEntity inbound, String boxCode,
|
||||
String boxCode,
|
||||
CartonMeasurementCommand command, long imageFileId) {
|
||||
InboundOrderEntity inbound = repository.lockInbound(tenantId, binding.inboundId());
|
||||
if (inbound == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
}
|
||||
if (!DEVICE_WRITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "测量已提交,设备不能新增或覆盖数据");
|
||||
}
|
||||
InboundMeasurementItemEntity item = repository.findItemByBoxCode(tenantId, boxCode);
|
||||
boolean created = item == null;
|
||||
InboundMeasurementEntity measurement;
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** Allocates stable carton codes and forwards immutable label facts to Print. */
|
||||
@Service
|
||||
public class InboundCartonLabelPrintService {
|
||||
private static final BigDecimal PAGE_MM = new BigDecimal("100");
|
||||
private static final List<String> SAFE_RESPONSE_HEADERS = List.of(
|
||||
HttpHeaders.CONTENT_DISPOSITION, HttpHeaders.CACHE_CONTROL, HttpHeaders.PRAGMA,
|
||||
"X-Print-Page-Count", "X-Request-Id", "X-Trace-Id");
|
||||
|
||||
private final InboundCartonLabelService labelService;
|
||||
private final PrintPdfClient printClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public InboundCartonLabelPrintService(InboundCartonLabelService labelService,
|
||||
PrintPdfClient printClient,
|
||||
ObjectMapper objectMapper) {
|
||||
this.labelService = labelService;
|
||||
this.printClient = printClient;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> generate(long tenantId, long inboundId,
|
||||
List<Integer> cartonIndexes, String requestId) {
|
||||
InboundCartonLabels.Batch batch = labelService.list(tenantId, inboundId, cartonIndexes);
|
||||
PrintPdfClient.InboundMarkPdfRequest request = new PrintPdfClient.InboundMarkPdfRequest(
|
||||
"INBOUND_MARK_V1", new PrintPdfClient.PageSpec(PAGE_MM, PAGE_MM),
|
||||
batch.inboundCode() + "-" + batch.totalCount() + "件-箱唛.pdf",
|
||||
batch.labels().stream().map(this::toPrintLabel).toList());
|
||||
try {
|
||||
return requirePdf(printClient.generateInboundMarks(internalToken, requestId, request));
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private PrintPdfClient.Label toPrintLabel(InboundCartonLabels.Label label) {
|
||||
return new PrintPdfClient.Label(label.topCode(), label.warehouseText(), label.cartonText(),
|
||||
label.routeText(), label.boxCode());
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> requirePdf(ResponseEntity<byte[]> response) {
|
||||
byte[] body = response == null || response.getBody() == null ? new byte[0] : response.getBody();
|
||||
MediaType contentType = response == null ? null : response.getHeaders().getContentType();
|
||||
if (contentType == null || !MediaType.APPLICATION_PDF.isCompatibleWith(contentType) || body.length == 0) {
|
||||
throw downstreamError(body);
|
||||
}
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
SAFE_RESPONSE_HEADERS.forEach(name -> {
|
||||
String value = response.getHeaders().getFirst(name);
|
||||
if (StringUtils.hasText(value)) headers.set(name, value);
|
||||
});
|
||||
headers.setContentLength(body.length);
|
||||
return ResponseEntity.ok().headers(headers).body(body);
|
||||
}
|
||||
|
||||
private BizException downstreamError(byte[] body) {
|
||||
try {
|
||||
ApiResponse<Object> error = objectMapper.readValue(body, new TypeReference<>() { });
|
||||
if (error != null && !error.successful()) {
|
||||
return new BizException(ResultCode.of(error.code()),
|
||||
StringUtils.hasText(error.message()) ? error.message() : "打印服务生成失败");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Non-contract responses are treated as unavailable instead of exposing downstream content.
|
||||
}
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务返回了无效响应");
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,7 @@ public class InboundMeasurementService {
|
||||
measurement == null ? null : measurement.getOriginalDocumentNo(),
|
||||
measurement == null ? null : measurement.getOriginalGrossWeightKg(),
|
||||
measurement == null ? null : measurement.getOriginalVolumeCbm(),
|
||||
inbound.getTotalPackageCount(),
|
||||
measurement == null ? inbound.getTotalPackageCount() : measurement.getTotalPackageCount(),
|
||||
inbound.getActualGrossWeightKg(),
|
||||
measurement == null ? inbound.getActualVolumeCbm() : measurement.getTotalVolumeCbm(),
|
||||
|
||||
@@ -68,6 +68,17 @@ public class MeasurementReviewService {
|
||||
if (!"SUBMITTED".equals(measurement.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "测量批次不是待审核状态");
|
||||
}
|
||||
if (approved) {
|
||||
long confirmed = repository.listMeasurementItems(tenantId, measurement.getId()).stream()
|
||||
.map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.mapToLong(Integer::longValue)
|
||||
.sum();
|
||||
if (order.getTotalPackageCount() == null || confirmed != order.getTotalPackageCount()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"人工确认箱数合计与入库单总箱数不一致,不能审核通过");
|
||||
}
|
||||
}
|
||||
|
||||
if (repository.transitionReview(tenantId, inboundId, operatorId,
|
||||
request.expectedVersion(), approved) != 1) {
|
||||
|
||||
@@ -33,6 +33,8 @@ baiyue:
|
||||
url: ${ORDER_SERVICE_URL:}
|
||||
file:
|
||||
url: ${FILE_SERVICE_URL:}
|
||||
print:
|
||||
url: ${PRINT_SERVICE_URL:}
|
||||
firstleg:
|
||||
inbound:
|
||||
allow-rejected-measurement: false
|
||||
|
||||
@@ -454,8 +454,12 @@ CREATE TABLE IF NOT EXISTS fl_price_book_channel (
|
||||
reference_transit VARCHAR(128) NULL COMMENT '参考时效',
|
||||
surcharge_description VARCHAR(2000) NULL COMMENT '附加费说明,仅展示',
|
||||
remark VARCHAR(2000) NULL COMMENT '渠道备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_book_channel (price_book_version_id, channel_code),
|
||||
KEY idx_price_book_channel_code (channel_code, price_book_version_id)
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -19,6 +20,8 @@ class InboundOrderControllerContractTest {
|
||||
AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
assertAuthority("printCartonLabels", "hasAuthority('platform:firstleg:inbound:view')",
|
||||
AuthenticatedUser.class, long.class, long.class, List.class, String.class);
|
||||
|
||||
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("价格版本渠道 Mapper 契约")
|
||||
class PriceBookChannelMapperTest {
|
||||
@Test
|
||||
@DisplayName("重新生成版本快照时使用物理删除避免唯一键冲突")
|
||||
void shouldPhysicallyDeleteVersionChannels() throws Exception {
|
||||
Method method = PriceBookChannelMapper.class
|
||||
.getMethod("physicallyDeleteByPriceBookVersionId", long.class);
|
||||
|
||||
assertThat(method.getAnnotation(Delete.class).value())
|
||||
.containsExactly("DELETE FROM fl_price_book_channel WHERE price_book_version_id = #{versionId}");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ class PriceBookSqlContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String migration = new ClassPathResource("db/phase2-fba-pricing-migration.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String channelTable = schema.substring(
|
||||
schema.indexOf("CREATE TABLE IF NOT EXISTS fl_price_book_channel"),
|
||||
schema.indexOf("ENGINE=InnoDB COMMENT='价格版本渠道及船期快照'")
|
||||
);
|
||||
assertThat(schema)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_channel_catalog")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_origin_alias")
|
||||
@@ -32,6 +36,11 @@ class PriceBookSqlContractTest {
|
||||
.contains("destination_name VARCHAR(128)")
|
||||
.contains("parse_issues_json JSON NULL")
|
||||
.contains("DEFAULT 'PENDING_CONFIRMATION'");
|
||||
assertThat(channelTable)
|
||||
.contains("created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID'")
|
||||
.contains("updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID'")
|
||||
.contains("deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记'")
|
||||
.contains("version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'");
|
||||
assertThat(migration)
|
||||
.doesNotContain("fl_price_book_version")
|
||||
.doesNotContain("'pricing_tax_mode'")
|
||||
|
||||
+2
-1
@@ -50,6 +50,7 @@ class CartonMeasurementServiceTest {
|
||||
when(resolver.resolveByBoxCode(10001L, "RK-001#5-1"))
|
||||
.thenReturn(new CartonLabelBindingResolver.Binding(70001L, 80001L, "RK-001", 1, 5, "50x40x30cm"));
|
||||
when(repository.findInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
|
||||
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
|
||||
when(images.upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()))
|
||||
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
|
||||
99001L, "measure.jpg", "image/jpeg", 10L, "NOT_REQUIRED", null, null, null, null)));
|
||||
@@ -130,7 +131,7 @@ class CartonMeasurementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedPackageCountCanBeCorrectedAfterSubmissionAndSyncsInboundSnapshot() {
|
||||
void confirmedPackageCountCanBeCorrectedAfterSubmissionWithoutChangingOriginalPackageCount() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "SUBMITTED", 2);
|
||||
measurement.setTotalPackageCount(1);
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InboundCartonLabelPrintServiceTest {
|
||||
private final InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
|
||||
private final PrintPdfClient print = mock(PrintPdfClient.class);
|
||||
private InboundCartonLabelPrintService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundCartonLabelPrintService(labels, print, new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardsStableLabelsAndOnlySafePdfHeaders() {
|
||||
when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.setContentDispositionFormData("attachment", "labels.pdf");
|
||||
headers.set("X-Print-Page-Count", "1");
|
||||
headers.set("X-Internal-Service-Token", "must-not-leak");
|
||||
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(new ResponseEntity<>(new byte[] {1, 2, 3}, headers,
|
||||
org.springframework.http.HttpStatus.OK));
|
||||
|
||||
ResponseEntity<byte[]> response = service.generate(10001L, 70001L, List.of(1), "request-1");
|
||||
|
||||
ArgumentCaptor<PrintPdfClient.InboundMarkPdfRequest> request =
|
||||
ArgumentCaptor.forClass(PrintPdfClient.InboundMarkPdfRequest.class);
|
||||
verify(print).generateInboundMarks(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.eq("request-1"), request.capture());
|
||||
assertThat(request.getValue().downloadFileName()).isEqualTo("PCHK2608040174-19件-箱唛.pdf");
|
||||
assertThat(request.getValue().labels()).singleElement().satisfies(label -> {
|
||||
assertThat(label.qrContent()).isEqualTo("PCHK2608040174#19-1");
|
||||
assertThat(label.cartonText()).isEqualTo("CTNS:19-1");
|
||||
});
|
||||
assertThat(response.getBody()).containsExactly(1, 2, 3);
|
||||
assertThat(response.getHeaders().getFirst("X-Print-Page-Count")).isEqualTo("1");
|
||||
assertThat(response.getHeaders()).doesNotContainKey("X-Internal-Service-Token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsPrintJsonBusinessFailureInsteadOfReturningItAsPdf() throws Exception {
|
||||
when(labels.list(10001L, 70001L, null)).thenReturn(batch());
|
||||
byte[] error = "{\"code\":\"A0422\",\"message\":\"标签字段超出模板限制\","
|
||||
.concat("\"data\":null,\"requestId\":\"req-2\",\"traceId\":\"trace-2\"}")
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(error));
|
||||
|
||||
assertThatThrownBy(() -> service.generate(10001L, 70001L, null, null))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("标签字段超出模板限制")
|
||||
.extracting(value -> ((BizException) value).getResultCode())
|
||||
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
}
|
||||
|
||||
private InboundCartonLabels.Batch batch() {
|
||||
InboundCartonLabels.Label label = new InboundCartonLabels.Label(
|
||||
1L, 2L, 1, "音箱", 19, "PCHK2608040174#19-1", 19, 1,
|
||||
"50x40x30cm", new BigDecimal("50"), new BigDecimal("40"), new BigDecimal("30"),
|
||||
"PCHK2608040174", "MCC1", "CTNS:19-1", "洛杉矶普船海卡");
|
||||
return new InboundCartonLabels.Batch(70001L, 80001L, "PCHK2608040174", 19, List.of(label));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.config.InboundProperties;
|
||||
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
@@ -79,6 +80,23 @@ class MeasurementReviewServiceTest {
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("确认箱数合计不等于订单总箱数时不能审核通过")
|
||||
void shouldRejectApprovalWhenConfirmedPackageCountDoesNotMatchOrder() {
|
||||
Fixture fixture = fixture(false);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setPackageCount(4);
|
||||
when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-mismatch",
|
||||
new MeasurementReviewRequest(20L, 3, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
|
||||
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("驳回原因必填且状态推进为测量驳回")
|
||||
void shouldRequireRejectReason() {
|
||||
@@ -175,10 +193,15 @@ class MeasurementReviewServiceTest {
|
||||
InboundOrderEntity order = new InboundOrderEntity();
|
||||
order.setProcessStatus("MEASURE_PENDING");
|
||||
order.setMeasureReviewStatus("PENDING");
|
||||
order.setTotalPackageCount(5);
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(20L);
|
||||
measurement.setStatus("SUBMITTED");
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setPackageCount(5);
|
||||
when(repository.findOwned(10001L, 10L)).thenReturn(order);
|
||||
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
|
||||
when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
||||
return new Fixture(service, repository, idempotency, order, measurement);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user