仓库:" + 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()) + "
合计:" + 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("