fix(lastleg): trace return carton sources and generate scannable labels

This commit is contained in:
2026-09-12 16:16:38 +08:00
parent e467d60e8f
commit f26e4b2972
13 changed files with 204 additions and 5 deletions
@@ -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;
+1
View File
@@ -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`。
+1
View File
@@ -5,6 +5,7 @@
<parent><groupId>com.baiyue.firstleg</groupId><artifactId>baiyue-firstleg-platform</artifactId><version>1.0.0-SNAPSHOT</version><relativePath>../pom.xml</relativePath></parent>
<artifactId>lastleg-service</artifactId>
<dependencies>
<dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.3</version></dependency>
<dependency><groupId>com.baiyue.shared</groupId><artifactId>common-event-contract</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>com.baiyue.shared</groupId><artifactId>baiyue-common-openapi</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency>
@@ -37,6 +37,13 @@ public class MerchantReturnInventoryController {
return ApiResponse.success(service.summaryPage(customer(user), request));
}
@GetMapping("/cartons")
@PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')")
public ApiResponse<PageResult<ReturnInventoryResponses.CartonSummary>> 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<ReturnInventoryResponses.CartonDetail> carton(@AuthenticationPrincipal AuthenticatedUser user,
@@ -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) { }
@@ -9,6 +9,21 @@ import java.util.List;
@Mapper
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
String MERCHANT_FILTER = " FROM ll_return_inventory_carton c WHERE c.customer_id=#{customerId} " +
"<if test='query.overseasWarehouseId != null and query.overseasWarehouseId != \"\"'>AND c.overseas_warehouse_id=#{query.overseasWarehouseId} </if>" +
"<if test='query.cartonMark != null and query.cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{query.cartonMark},'%') </if>" +
"<if test='query.returnNo != null and query.returnNo != \"\"'>AND c.source_return_no LIKE CONCAT('%',#{query.returnNo},'%') </if>" +
"<if test='query.status != null and query.status != \"\"'>AND c.status=#{query.status} </if>" +
"<if test='query.skuCode != null and query.skuCode != \"\"'>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},'%')) </if>";
@Select("<script>SELECT COUNT(*)" + MERCHANT_FILTER + "</script>")
long merchantCount(@Param("customerId") String customerId,
@Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query);
@Select("<script>SELECT c.*" + MERCHANT_FILTER + "ORDER BY c.created_at DESC,c.id DESC LIMIT #{offset},#{query.size}</script>")
List<ReturnInventoryCarton> 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<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
@@ -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("<svg xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"箱号条码\" viewBox=\"0 0 ")
.append(width).append(" 50\" preserveAspectRatio=\"none\" style=\"display:block;width:100%;height:15mm\">")
.append("<rect width=\"100%\" height=\"100%\" fill=\"white\"/>");
for (int index = 0; index < modules.length;) {
if (!modules[index]) { index++; continue; }
int start = index;
while (index < modules.length && modules[index]) index++;
svg.append("<rect x=\"").append(start + QUIET_ZONE).append("\" y=\"0\" width=\"")
.append(index - start).append("\" height=\"50\" fill=\"black\"/>");
}
return svg.append("</svg>").toString();
}
}
@@ -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<ReturnInventoryResponses.CartonSummary> cartonPage(String customerId, ReturnInventoryRequests.Query request) {
required(customerId, "客户身份不能为空");
long total = cartons.merchantCount(customerId, request);
List<ReturnInventoryResponses.CartonSummary> 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<ReturnInventoryResponses.SummaryItem> summaryPage(String customerId, ReturnInventoryRequests.SummaryQuery request) {
String warehouseId = text(request.overseasWarehouseId());
@@ -230,6 +243,8 @@ public class ReturnInventoryService {
}
Map<String, Long> remaining = new LinkedHashMap<>(requested);
Map<String, Integer> emptiedBySku = new LinkedHashMap<>();
LinkedHashSet<String> consumedBatchNos = new LinkedHashSet<>();
LinkedHashSet<String> consumedReturnNos = new LinkedHashSet<>();
long total = 0;
for (ReturnInventoryCarton source : sources) {
List<ReturnInventoryCartonItem> 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<String, Long> entry : requested.entrySet()) {
ReturnInventoryCartonItem targetItem = new ReturnInventoryCartonItem();
@@ -312,13 +329,14 @@ public class ReturnInventoryService {
for (ReturnInventoryResponses.CartonItem item : items) rows.append("<tr><td>").append(escape(item.skuCode())).append("</td><td>")
.append(escape(item.productNameZh())).append("</td><td class=\"qty\">").append(item.quantity()).append("</td></tr>");
return "<!doctype html><html><head><meta charset=\"utf-8\"><title>退货箱唛</title><style>"
+ "@page{size:100mm 150mm;margin:5mm}body{font-family:Arial,'Microsoft YaHei',sans-serif;color:#111;margin:0}.label{border:2px solid #111;padding:5mm;min-height:136mm}.brand{font-size:16px;font-weight:700}.title{font-size:22px;font-weight:700;margin:3mm 0 5mm}.mark{font:700 18px monospace;letter-spacing:1px;word-break:break-all}.meta{font-size:11px;margin:4mm 0;border-top:1px solid #111;border-bottom:1px solid #111;padding:3mm 0}.barcode{height:11mm;margin:3mm 0;background:repeating-linear-gradient(90deg,#111 0 2px,#fff 2px 4px,#111 4px 5px,#fff 5px 8px)}table{width:100%;border-collapse:collapse;font-size:11px}th,td{border:1px solid #111;padding:2mm;text-align:left}.qty{text-align:right;font-weight:700}.total{font-size:14px;font-weight:700;text-align:right;margin-top:4mm}</style></head><body><div class=\"label\"><div class=\"brand\">BAIYUE OVERSEAS WAREHOUSE</div><div class=\"title\">RETURN CARTON LABEL / 退货箱唛</div><div class=\"mark\">"
+ escape(carton.getCartonMark()) + "</div><div class=\"barcode\" aria-label=\"" + escape(carton.getCartonMark()) + "\"></div>"
+ "<div class=\"meta\">仓库:" + escape(carton.getOverseasWarehouseId()) + "<br>客户:" + escape(carton.getCustomerId())
+ "<br>箱规:" + 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}</style></head><body><div class=\"label\"><div class=\"brand\">BAIYUE OVERSEAS WAREHOUSE</div><div class=\"title\">RETURN CARTON LABEL / 退货箱唛</div><div class=\"mark\">"
+ escape(carton.getCartonMark()) + "</div><div class=\"barcode\">" + ReturnCartonBarcode.svg(carton.getCartonMark()) + "</div>"
+ "<div class=\"meta\">来源退货单:" + escape(carton.getSourceReturnNo())
+ "<br>箱规:" + dimension(carton.getLength()) + " × " + dimension(carton.getWidth()) + " × " + dimension(carton.getHeight()) + " cm / " + dimension(carton.getWeight()) + " kg"
+ "<br>签收批次:" + escape(carton.getSourceBatchNo()) + "</div><table><thead><tr><th>SKU</th><th>品名</th><th>数量</th></tr></thead><tbody>"
+ rows + "</tbody></table><div class=\"total\">合计:" + carton.getTotalQuantity() + " 件 / " + carton.getSkuTypes() + " 种 SKU</div></div></body></html>";
}
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); } }
@@ -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;
@@ -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: 客户查询退货箱明细
@@ -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("<rect x=\"(\\d+)\" y=\"0\" width=\"(\\d+)\" height=\"50\" fill=\"black\"/>").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");
}
}
@@ -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");
}
}
}
@@ -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("<svg", "RT-1", "RB-1,RB-2", "—", "box-sizing:border-box");
assertThat(html).doesNotContain("repeating-linear-gradient", "null", carton.getCustomerId(), carton.getOverseasWarehouseId());
}
@Test
void cartonPageCountsPhysicalCartonsAndKeepsTheAuthenticatedCustomerScope() {
var query = new ReturnInventoryRequests.Query(2, 1, "42", "SKU", null, "AVAILABLE", "RT-1");
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setCartonMark("RC-SECOND");
carton.setSkuTypes(2);
carton.setTotalQuantity(5L);
when(cartons.merchantCount("10001", query)).thenReturn(2L);
when(cartons.merchantPage("10001", query, 1L)).thenReturn(List.of(carton));
var result = service.cartonPage("10001", query);
assertThat(result.total()).isEqualTo(2);
assertThat(result.records()).hasSize(1);
assertThat(result.records().get(0).skuTypes()).isEqualTo(2);
assertThat(result.records().get(0).totalQuantity()).isEqualTo(5);
verify(cartons).merchantPage("10001", query, 1L);
}
@Test
void receiptCartonKeepsTheSourceReturnNumber() {
ReturnOrder order = completedOrder();
@@ -85,6 +120,8 @@ class ReturnInventoryServiceTest {
source.setCustomerId("10001");
source.setOverseasWarehouseId("42");
source.setCartonMark("RC-SOURCE");
source.setSourceBatchNo("RB-1");
source.setSourceReturnNo("RT-1");
source.setStatus(ReturnInventoryService.AVAILABLE);
source.setSkuTypes(2);
source.setTotalQuantity(4L);
@@ -109,6 +146,10 @@ class ReturnInventoryServiceTest {
verify(balances).adjustCartonQuantity("10001", "42", "SKU-B", 1L);
assertThat(source.getTotalQuantity()).isEqualTo(2L);
assertThat(source.getStatus()).isEqualTo(ReturnInventoryService.AVAILABLE);
ArgumentCaptor<ReturnInventoryCarton> 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() {