diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 0032dc897..0b8f14497 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -2530,6 +2530,7 @@ PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inven ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED','OUTBOUNDED')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no')); PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt; +ALTER TABLE ll_return_inventory_carton MODIFY COLUMN source_batch_no TEXT NULL COMMENT '实际消耗来源签收批次号,多个批次逗号分隔', MODIFY COLUMN source_return_no TEXT NULL COMMENT '实际消耗来源退货单号,多个单号逗号分隔'; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='fba_carton_mark'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN fba_carton_mark VARCHAR(255) NULL COMMENT ''FBA箱唛'' AFTER carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT ''目的仓编码'' AFTER fba_carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_name VARCHAR(255) NULL COMMENT ''目的仓名称'' AFTER destination_warehouse_code')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/lastleg-service/AGENTS.md b/lastleg-service/AGENTS.md index 7cf2c759c..e215e0bd1 100644 --- a/lastleg-service/AGENTS.md +++ b/lastleg-service/AGENTS.md @@ -15,6 +15,7 @@ - PDA 后端签收 `returnreceipt/controller/PdaReturnReceiptController.java`,前缀 `/api/pda/v1/last-leg/return-receipts`,依次 `start/sku-candidates/confirm`;按冻结仓和退货单校验实收,不允许未预报 SKU 或超量,幂等重放不得重复入账。确认同时生成签收箱与 SKU 库存;设备页面是否接通另查 PDA 工作台,不能以 Controller 存在代替前端接入。 - 库存/箱号 `returnreceipt/controller/PlatformReturnInventoryController.java`、`MerchantReturnInventoryController.java` → `returnreceipt/service/ReturnInventoryService.java`,前缀分别为 `/api/platform/v1/last-leg/return-inventory`、`/api/merchant/v1/last-leg/return-inventory`;PDA 合箱入口 `PdaReturnInventoryController.java` 的 `/packing/orders`、`/cartons/merge`、`/cartons/{cartonMark}/label`。同仓同客户选取退货单及可用 SKU,合箱扣减来源箱数量并生成目标箱,空来源箱标为 `MERGED`,不得跨客户或超出可用量。 - 退货出库继续进入 `delivery/service/DeliveryTransferService.java` 的 `RETURN_OUTBOUND`,按退货库存可用箱唛创建与拣货;不能与 `exceptionorder/` 的派送退回恢复库存混用。契约见 `src/main/resources/openapi/overseas-business-v1.yaml`,表入口 `src/main/resources/db/lastleg.sql` 的 `ll_return_*`。 +- 客户箱号管理使用 `GET /api/merchant/v1/last-leg/return-inventory/cartons` 按实物箱分页,库存箱内 SKU 明细仍使用根查询;两者均按登录客户精确隔离。合箱 `sourceBatchNo/sourceReturnNo` 保存实际消耗来源的去重业务编号(多个以逗号分隔),不得写状态或包含未取货订单。`ReturnCartonBarcode` 按箱号生成 Code 128 SVG,打印内容不得使用内部客户/仓库 ID。 人工计划自动生成托盘与资源重试:`inboundplan/controller/InboundPlanController.java` → `inboundplan/service/ContainerInboundPlanConversionCoordinator.java` 的 `convertManualAndAutoMaterialize`;保存单票后重读全柜预览,全员为 `PLANNED/RESOURCE_PENDING`、预览就绪且存在待绑定资源时自动调用柜级 `confirm`。一票柜立即触发,多票柜在最后一票齐全后触发;资源异常沿 `resourcePendingPlanCount` 和原柜级预览/确认入口重试,不新增独立正常人工步骤。回归入口为 `src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java`。 diff --git a/lastleg-service/pom.xml b/lastleg-service/pom.xml index 01edad467..1314eb0d7 100644 --- a/lastleg-service/pom.xml +++ b/lastleg-service/pom.xml @@ -5,6 +5,7 @@ com.baiyue.firstlegbaiyue-firstleg-platform1.0.0-SNAPSHOT../pom.xml lastleg-service + com.google.zxingcore3.5.3 com.baiyue.sharedcommon-event-contract${project.version} com.baiyue.sharedbaiyue-common-openapi${project.version} com.baomidoumybatis-plus-spring-boot3-starter diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java index 144050b21..0713b49bc 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java @@ -37,6 +37,13 @@ public class MerchantReturnInventoryController { return ApiResponse.success(service.summaryPage(customer(user), request)); } + @GetMapping("/cartons") + @PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')") + public ApiResponse> cartons(@AuthenticationPrincipal AuthenticatedUser user, + @Valid ReturnInventoryRequests.Query request) { + return ApiResponse.success(service.cartonPage(customer(user), request)); + } + @GetMapping("/cartons/{cartonMark}") @PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')") public ApiResponse carton(@AuthenticationPrincipal AuthenticatedUser user, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java index deb12c93f..5130349d8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java @@ -7,6 +7,11 @@ import java.util.List; public final class ReturnInventoryResponses { private ReturnInventoryResponses() { } + public record CartonSummary(String cartonMark, String overseasWarehouseId, String sourceBatchNo, + String sourceReturnNo, String status, int skuTypes, long totalQuantity, + BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight, + LocalDateTime createdAt) { } + public record InventoryItem(String overseasWarehouseId, String cartonMark, String sourceBatchNo, String status, String skuCode, String productNameZh, long quantity, int cartonSkuTypes, long cartonTotalQuantity, LocalDateTime receivedAt) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java index d2e200ab5..c2ddec712 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java @@ -9,6 +9,21 @@ import java.util.List; @Mapper public interface ReturnInventoryCartonMapper extends BaseMapper { + String MERCHANT_FILTER = " FROM ll_return_inventory_carton c WHERE c.customer_id=#{customerId} " + + "AND c.overseas_warehouse_id=#{query.overseasWarehouseId} " + + "AND c.carton_mark LIKE CONCAT('%',#{query.cartonMark},'%') " + + "AND c.source_return_no LIKE CONCAT('%',#{query.returnNo},'%') " + + "AND c.status=#{query.status} " + + "AND EXISTS(SELECT 1 FROM ll_return_inventory_carton_item i WHERE i.carton_id=c.id AND i.sku_code LIKE CONCAT('%',#{query.skuCode},'%')) "; + + @Select("") + long merchantCount(@Param("customerId") String customerId, + @Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query); + + @Select("") + List merchantPage(@Param("customerId") String customerId, + @Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query, @Param("offset") long offset); + @Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='AVAILABLE' ORDER BY created_at DESC,id DESC LIMIT 500") List findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId); @Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcode.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcode.java new file mode 100644 index 000000000..7811d8d63 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcode.java @@ -0,0 +1,26 @@ +package com.baiyue.lastleg.returnreceipt.service; + +import com.google.zxing.oned.Code128Writer; + +/** Inline vector bars remain printable even when browser background printing is disabled. */ +final class ReturnCartonBarcode { + private static final int QUIET_ZONE = 10; + + private ReturnCartonBarcode() { } + + static String svg(String cartonMark) { + boolean[] modules = new Code128Writer().encode(cartonMark); + int width = modules.length + QUIET_ZONE * 2; + StringBuilder svg = new StringBuilder("") + .append(""); + for (int index = 0; index < modules.length;) { + if (!modules[index]) { index++; continue; } + int start = index; + while (index < modules.length && modules[index]) index++; + svg.append(""); + } + return svg.append("").toString(); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java index 03e3ba1e6..4daa6e0b5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java @@ -91,6 +91,19 @@ public class ReturnInventoryService { return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size()); } + @Transactional(readOnly = true) + public PageResult cartonPage(String customerId, ReturnInventoryRequests.Query request) { + required(customerId, "客户身份不能为空"); + long total = cartons.merchantCount(customerId, request); + List records = total == 0 ? List.of() : + cartons.merchantPage(customerId, request, (request.page() - 1) * request.size()).stream() + .map(carton -> new ReturnInventoryResponses.CartonSummary(carton.getCartonMark(), carton.getOverseasWarehouseId(), + carton.getSourceBatchNo(), carton.getSourceReturnNo(), carton.getStatus(), + carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), + carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt())).toList(); + return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size()); + } + @Transactional(readOnly = true) public PageResult summaryPage(String customerId, ReturnInventoryRequests.SummaryQuery request) { String warehouseId = text(request.overseasWarehouseId()); @@ -230,6 +243,8 @@ public class ReturnInventoryService { } Map remaining = new LinkedHashMap<>(requested); Map emptiedBySku = new LinkedHashMap<>(); + LinkedHashSet consumedBatchNos = new LinkedHashSet<>(); + LinkedHashSet consumedReturnNos = new LinkedHashSet<>(); long total = 0; for (ReturnInventoryCarton source : sources) { List sourceItems = cartonItems.listCarton(source.getId()); @@ -238,6 +253,8 @@ public class ReturnInventoryService { String sku = item.getSkuCode().toUpperCase(java.util.Locale.ROOT); long take = Math.min(value(remaining.get(sku)), value(item.getQuantity())); if (take > 0) { + consumedBatchNos.add(required(source.getSourceBatchNo(), "来源签收批次缺失,无法合箱")); + consumedReturnNos.add(required(source.getSourceReturnNo(), "来源退货单缺失,无法合箱")); remaining.put(sku, remaining.get(sku) - take); total = Math.addExact(total, take); productNames.putIfAbsent(sku, item.getProductNameZh()); item.setQuantity(item.getQuantity() - take); @@ -256,7 +273,7 @@ public class ReturnInventoryService { long targetId = IdWorker.getId(); ReturnInventoryCarton target = new ReturnInventoryCarton(); target.setId(targetId); target.setCustomerId(customerId); target.setOverseasWarehouseId(sources.get(0).getOverseasWarehouseId()); - target.setCartonMark(numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId)); target.setSourceBatchNo("MERGED"); target.setSourceReturnNo(String.join(",", returnNos)); target.setStatus(AVAILABLE); + target.setCartonMark(numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId)); target.setSourceBatchNo(String.join(",", consumedBatchNos)); target.setSourceReturnNo(String.join(",", consumedReturnNos)); target.setStatus(AVAILABLE); target.setSkuTypes(requested.size()); target.setTotalQuantity(total); target.setLength(request.length()); target.setWidth(request.width()); target.setHeight(request.height()); target.setWeight(request.weight()); target.setCreatedAt(now); target.setUpdatedAt(now); cartons.insert(target); for (Map.Entry entry : requested.entrySet()) { ReturnInventoryCartonItem targetItem = new ReturnInventoryCartonItem(); @@ -312,13 +329,14 @@ public class ReturnInventoryService { for (ReturnInventoryResponses.CartonItem item : items) rows.append("").append(escape(item.skuCode())).append("") .append(escape(item.productNameZh())).append("").append(item.quantity()).append(""); return "退货箱唛
BAIYUE OVERSEAS WAREHOUSE
RETURN CARTON LABEL / 退货箱唛
" - + escape(carton.getCartonMark()) + "
" - + "
仓库:" + escape(carton.getOverseasWarehouseId()) + "
客户:" + escape(carton.getCustomerId()) - + "
箱规:" + escape(String.valueOf(carton.getLength())) + " × " + escape(String.valueOf(carton.getWidth())) + " × " + escape(String.valueOf(carton.getHeight())) + " cm / " + escape(String.valueOf(carton.getWeight())) + " kg" + + "@page{size:100mm 150mm;margin:5mm}body{font-family:Arial,'Microsoft YaHei',sans-serif;color:#111;margin:0}.label{box-sizing:border-box;width:90mm;border:2px solid #111;padding:3mm;min-height:138mm}.brand{font-size:12px;font-weight:700}.title{font-size:16px;font-weight:700;margin:3mm 0}.mark{font:700 16px monospace;word-break:break-all}.meta{font-size:11px;margin:3mm 0;border-top:1px solid #111;border-bottom:1px solid #111;padding:2mm 0;overflow-wrap:anywhere}.barcode{margin:3mm 0}table{width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed}th,td{border:1px solid #111;padding:1mm;text-align:left;overflow-wrap:anywhere}tr{break-inside:avoid}.qty{text-align:right;font-weight:700}.total{font-size:14px;font-weight:700;text-align:right;margin-top:3mm}
BAIYUE OVERSEAS WAREHOUSE
RETURN CARTON LABEL / 退货箱唛
" + + escape(carton.getCartonMark()) + "
" + ReturnCartonBarcode.svg(carton.getCartonMark()) + "
" + + "
来源退货单:" + escape(carton.getSourceReturnNo()) + + "
箱规:" + dimension(carton.getLength()) + " × " + dimension(carton.getWidth()) + " × " + dimension(carton.getHeight()) + " cm / " + dimension(carton.getWeight()) + " kg" + "
签收批次:" + escape(carton.getSourceBatchNo()) + "
" + rows + "
SKU品名数量
合计:" + carton.getTotalQuantity() + " 件 / " + carton.getSkuTypes() + " 种 SKU
"; } + private static String dimension(BigDecimal value) { return value == null ? "—" : value.stripTrailingZeros().toPlainString(); } private String hash(Object request) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") .digest(String.valueOf(request).getBytes(StandardCharsets.UTF_8))); } catch (Exception exception) { throw new IllegalStateException("无法生成请求哈希", exception); } } diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 3718bfa44..e9a5afbcb 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -2437,6 +2437,7 @@ PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inven ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED','OUTBOUNDED')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no')); PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt; +ALTER TABLE ll_return_inventory_carton MODIFY COLUMN source_batch_no TEXT NULL COMMENT '实际消耗来源签收批次号,多个批次逗号分隔', MODIFY COLUMN source_return_no TEXT NULL COMMENT '实际消耗来源退货单号,多个单号逗号分隔'; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='fba_carton_mark'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN fba_carton_mark VARCHAR(255) NULL COMMENT ''FBA箱唛'' AFTER carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT ''目的仓编码'' AFTER fba_carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_name VARCHAR(255) NULL COMMENT ''目的仓名称'' AFTER destination_warehouse_code')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 2038a62d9..afc5a08c1 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -1393,6 +1393,21 @@ paths: - { name: overseasWarehouseId, in: query, schema: { type: string, maxLength: 128 } } - { name: skuCode, in: query, schema: { type: string, maxLength: 128 } } responses: { '200': { description: 当前客户 SKU 汇总库存分页 } } + /api/merchant/v1/last-leg/return-inventory/cartons: + get: + summary: 客户退货箱号分页 + description: 需要 merchant:overseas-warehouse:return-inventory:list。客户范围仅取登录身份且精确匹配;每个实物箱一条记录,SKU 筛选使用存在性条件,不重复箱号或放大总箱数。 + security: [{ bearerAuth: [] }] + parameters: + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } } + - { name: overseasWarehouseId, in: query, schema: { type: string } } + - { name: skuCode, in: query, schema: { type: string } } + - { name: cartonMark, in: query, schema: { type: string } } + - { name: returnNo, in: query, schema: { type: string } } + - { name: status, in: query, schema: { type: string } } + responses: + '200': { description: 分页 records 每箱一行,包含 cartonMark、overseasWarehouseId、sourceBatchNo、sourceReturnNo、status、skuTypes、totalQuantity、length、width、height、weight、createdAt;total 为实际箱数 } /api/merchant/v1/last-leg/return-inventory/cartons/{cartonMark}: get: summary: 客户查询退货箱明细 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcodeTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcodeTest.java new file mode 100644 index 000000000..6671baf2b --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcodeTest.java @@ -0,0 +1,36 @@ +package com.baiyue.lastleg.returnreceipt.service; + +import com.google.zxing.common.BitArray; +import com.google.zxing.oned.Code128Reader; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +class ReturnCartonBarcodeTest { + @ParameterizedTest + @ValueSource(strings = {"RC-WH-20260912-480898", "RC-WH-20260912-274178", "RC-1", "RC-MERGED"}) + void decodesTheActualRenderedSvgBarsToTheExactCartonMark(String mark) throws Exception { + String svg = ReturnCartonBarcode.svg(mark); + var viewBox = Pattern.compile("viewBox=\"0 0 (\\d+) 50\"").matcher(svg); + assertThat(viewBox.find()).isTrue(); + int width = Integer.parseInt(viewBox.group(1)); + BitArray scanline = new BitArray(width); + var bars = Pattern.compile("").matcher(svg); + int firstBar = width; + int lastBar = 0; + while (bars.find()) { + int start = Integer.parseInt(bars.group(1)); + int end = start + Integer.parseInt(bars.group(2)); + firstBar = Math.min(firstBar, start); + lastBar = Math.max(lastBar, end); + scanline.setRange(start, end); + } + assertThat(firstBar).isGreaterThanOrEqualTo(10); + assertThat(width - lastBar).isGreaterThanOrEqualTo(10); + assertThat(new Code128Reader().decodeRow(0, scanline, null).getText()).isEqualTo(mark); + assertThat(svg).doesNotContain("background", "repeating-linear-gradient"); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonQueryTest.java new file mode 100644 index 000000000..491b1b4c7 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonQueryTest.java @@ -0,0 +1,32 @@ +package com.baiyue.lastleg.returnreceipt.service; + +import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ReturnCartonQueryTest { + @Test + void bothQueriesUseExactCustomerScopeAndSkuExistenceWithoutAnOuterJoin() throws Exception { + var query = new ReturnInventoryRequests.Query(2, 1, "42", "SKU", "RC", "AVAILABLE", "RT"); + for (String name : new String[]{"merchantCount", "merchantPage"}) { + var method = java.util.Arrays.stream(ReturnInventoryCartonMapper.class.getMethods()) + .filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow(); + String script = String.join(" ", method.getAnnotation(Select.class).value()); + var source = new XMLLanguageDriver().createSqlSource(new Configuration(), script, Map.class); + var bound = source.getBoundSql(Map.of("customerId", "10001", "query", query, "offset", 1L)); + String sql = bound.getSql().replaceAll("\\s+", " "); + assertThat(sql).contains("WHERE c.customer_id=?", "AND EXISTS(SELECT 1 FROM ll_return_inventory_carton_item"); + assertThat(sql).doesNotContain("customer_id LIKE", " JOIN "); + assertThat(bound.getParameterMappings().get(0).getProperty()).isEqualTo("customerId"); + if (name.equals("merchantPage")) assertThat(sql).contains("ORDER BY c.created_at DESC,c.id DESC LIMIT ?,?"); + else assertThat(sql).startsWith("SELECT COUNT(*) FROM ll_return_inventory_carton"); + } + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java index dc5f6338b..b156f9237 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java @@ -59,6 +59,41 @@ class ReturnInventoryServiceTest { ReflectionTestUtils.setField(service, "receiptMapper", receipts); } + @Test + void printableLabelUsesBusinessNumbersAndShowsMissingDimensionsWithoutNullOrInternalIds() { + ReturnInventoryCarton carton = new ReturnInventoryCarton(); + carton.setId(31L); + carton.setCartonMark("RC-WH-20260912-480898"); + carton.setCustomerId("2098676663424974850"); + carton.setOverseasWarehouseId("193000000000200003"); + carton.setSourceReturnNo("RT-1"); + carton.setSourceBatchNo("RB-1,RB-2"); + carton.setTotalQuantity(2L); + carton.setSkuTypes(1); + when(cartons.findCustomerMark(carton.getCustomerId(), carton.getCartonMark())).thenReturn(carton); + when(cartonItems.listCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L))); + String html = service.label(carton.getCustomerId(), carton.getCartonMark()).printableHtml(); + assertThat(html).contains(" target = ArgumentCaptor.forClass(ReturnInventoryCarton.class); + verify(cartons).insert(target.capture()); + assertThat(target.getValue().getSourceBatchNo()).isEqualTo("RB-1"); + assertThat(target.getValue().getSourceReturnNo()).isEqualTo("RT-1"); } private ReturnOrder completedOrder() {