This commit is contained in:
2026-09-15 18:17:06 +08:00
parent 45018987ef
commit 4b671bb0ab
13 changed files with 174 additions and 15 deletions
@@ -11,6 +11,9 @@ import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.UUID;
import java.math.BigInteger;
import java.nio.ByteBuffer;
@Component
public class LastlegOrderNoGenerator {
@@ -32,12 +35,31 @@ public class LastlegOrderNoGenerator {
}
public String next(String businessType, String warehouse, long uniqueId) {
String code = businessType == null ? "" : businessType.trim().toUpperCase(Locale.ROOT);
String returnPrefix = switch (code) {
case "RETURN" -> "RT";
case "RETURN_RECEIPT" -> "RR";
case "RETURN_BATCH" -> "RB";
case "RETURN_CARTON" -> "RC";
case "RETURN_RELABEL" -> "RETU";
default -> null;
};
// Return numbers are independent business identifiers, not truncated database IDs.
// Keep every UUID bit; the longest result (RETU + 25 base-36 digits) fits VARCHAR(32).
if (returnPrefix != null) return returnPrefix + "-" + encodeReturnUuid(UUID.randomUUID());
String type = normalizeType(businessType);
return type + "-" + warehouseCode(warehouse) + "-"
+ LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
+ String.format(Locale.ROOT, "%06d", Math.floorMod(uniqueId, 1_000_000));
}
static String encodeReturnUuid(UUID uuid) {
byte[] bytes = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits())
.putLong(uuid.getLeastSignificantBits()).array();
String encoded = new BigInteger(1, bytes).toString(36).toUpperCase(Locale.ROOT);
return "0".repeat(25 - encoded.length()) + encoded;
}
private String normalizeType(String value) {
String code = value == null ? "LMO" : value.trim().toUpperCase(Locale.ROOT);
return switch (code) {
@@ -54,10 +76,6 @@ public class LastlegOrderNoGenerator {
case "DISCARD" -> "CD";
case "PROCUREMENT" -> "PR";
case "INBOUND_PLAN" -> "IP";
case "RETURN" -> "RT";
case "RETURN_RECEIPT" -> "RR";
case "RETURN_BATCH" -> "RB";
case "RETURN_CARTON" -> "RC";
default -> {
String normalized = code.replaceAll("[^A-Z0-9]", "");
yield normalized.isEmpty() ? "LMO" : normalized.substring(0, Math.min(4, normalized.length()));
@@ -1421,7 +1421,7 @@ paths:
responses: { '200': { description: 当前客户退货订单分页 } }
post:
summary: 客户创建退货预报订单
description: 客户只预报不重复的 SKU 和预计数量,不要求提前知道快递单号;仓库 PDA 实收数量才是实际入库事实。
description: 客户只预报不重复的 SKU 和预计数量,不要求提前知道快递单号;仓库 PDA 实收数量才是实际入库事实。系统退货编号由业务前缀和25位大写Base36完整随机UUID编码组成,退货单RT、签收单RR、批次RB、箱唛RC及退货主单RETU均独立生成,不编码内部ID、仓库或日期。幂等重放返回已保存的编号,不重新生成。
security: [{ bearerAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReturnOrderCreate' } } } }
responses: { '200': { description: 创建后的退货订单详情 }, '409': { description: 幂等键冲突 } }
@@ -11,7 +11,7 @@ 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"})
@ValueSource(strings = {"RC-0000000000000000000000000", "RC-F5LXX1ZZ5PNORYNQGLHZMSP33", "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);
@@ -0,0 +1,59 @@
package com.baiyue.lastleg.service;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
class ReturnBusinessNumberTest {
private static final Map<String, String> TYPES = Map.of("RETURN", "RT", "RETURN_RECEIPT", "RR",
"RETURN_BATCH", "RB", "RETURN_CARTON", "RC", "RETURN_RELABEL", "RETU");
@Test
void formerlyCollidingInternalIdsAndRepeatedIdsNoLongerDetermineBusinessNumbers() {
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
long first = 2099800000000781889L, second = first + 1_000_000L;
assertEquals(Math.floorMod(first, 1_000_000), Math.floorMod(second, 1_000_000));
TYPES.forEach((type, prefix) -> {
String a = generator.next(type, "warehouse-LAX-01", first);
String b = generator.next(type, "warehouse-LAX-01", second);
String c = generator.next(type, "warehouse-LAX-01", first);
assertEquals(3, new HashSet<>(java.util.List.of(a, b, c)).size());
for (String value : java.util.List.of(a, b, c)) {
assertTrue(value.matches(prefix + "-[0-9A-Z]{25}"), value);
assertTrue(value.length() <= 32);
assertFalse(value.contains(Long.toString(first)));
}
});
}
@Test
void encodingPreservesAll128BitsIncludingLeadingZerosAndSignBits() {
for (UUID uuid : java.util.List.of(new UUID(0, 0), new UUID(-1, -1), new UUID(Long.MIN_VALUE, 1),
UUID.fromString("00000000-0000-4000-8000-000000000001"),
UUID.fromString("ffffffff-ffff-4fff-bfff-000000000001"))) {
String encoded = LastlegOrderNoGenerator.encodeReturnUuid(uuid);
assertEquals(25, encoded.length());
assertEquals(new BigInteger(uuid.toString().replace("-", ""), 16), new BigInteger(encoded, 36));
}
}
@Test
void independentInstancesGenerateDistinctNumbersDuringConcurrentBurst() {
var values = IntStream.range(0, 20_000).parallel()
.mapToObj(i -> new LastlegOrderNoGenerator().next("RETURN_RECEIPT", "42", 123L)).toList();
assertEquals(values.size(), new HashSet<>(values).size());
}
@Test
void nonReturnRulesRemainUnchangedAndReturnNormalizationIsLocaleIndependent() {
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
for (String type : java.util.List.of("OUTBOUND", "RELABEL", "PROCUREMENT", "DISPATCH")) {
assertEquals(generator.next(type, "LAX", 123), generator.next(type, "LAX", 1_000_123));
}
assertTrue(generator.next(" return_receipt ", "42", 1).matches("RR-[0-9A-Z]{25}"));
}
}