diff --git a/rc-service/rc-service-order/order-adapter/src/test/java/com/red/circle/order/adapter/app/PayServerNoticeControllerTest.java b/rc-service/rc-service-order/order-adapter/src/test/java/com/red/circle/order/adapter/app/PayServerNoticeControllerTest.java index eefd4406..4d059202 100644 --- a/rc-service/rc-service-order/order-adapter/src/test/java/com/red/circle/order/adapter/app/PayServerNoticeControllerTest.java +++ b/rc-service/rc-service-order/order-adapter/src/test/java/com/red/circle/order/adapter/app/PayServerNoticeControllerTest.java @@ -10,11 +10,11 @@ public class PayServerNoticeControllerTest { @Test public void v5PayJsonParserPreservesSignedNumberText() throws Exception { Map fields = PayServerNoticeController.parseV5PayJsonFields( - "{\"amount\":14400.00,\"status\":2,\"orderNo\":\"ASLAN-123\"}" + "{\"amount\":14400.00,\"status\":2,\"orderNo\":\"ASLAN123\"}" .getBytes(StandardCharsets.UTF_8)); Assert.assertEquals("14400.00", fields.get("amount")); Assert.assertEquals("2", fields.get("status")); - Assert.assertEquals("ASLAN-123", fields.get("orderNo")); + Assert.assertEquals("ASLAN123", fields.get("orderNo")); } } diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNoUtils.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNoUtils.java index 7dbd918a..1dbe20a7 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNoUtils.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNoUtils.java @@ -2,29 +2,61 @@ package com.red.circle.order.app.common; import java.util.Locale; import java.util.Objects; +import java.util.regex.Pattern; /** 共享 V5Pay 商户时隔离 Aslan 与其他系统订单号。 */ public final class V5PayOrderNoUtils { + private static final String DEFAULT_PREFIX = "ASLAN"; + private static final Pattern PREFIX_PATTERN = Pattern.compile("[A-Za-z0-9]+"); + private static final Pattern LOCAL_ORDER_NO_PATTERN = Pattern.compile("[1-9][0-9]{0,18}"); + private V5PayOrderNoUtils() { } public static String providerOrderNo(String prefix, String localOrderNo) { - return normalizedPrefix(prefix) + "-" + Objects.toString(localOrderNo, "").trim(); + String normalizedPrefix = normalizedPrefix(prefix); + String normalizedLocalOrderNo = Objects.toString(localOrderNo, "").trim(); + if (!isValidLocalOrderNo(normalizedLocalOrderNo)) { + // Aslan 订单由 IdWorkerUtils 生成正数 long;提前拒绝非当前格式,避免把无效单号送到支付方。 + throw new IllegalArgumentException("V5Pay local orderNo must be a positive numeric ID"); + } + // V5Pay orderNo 只允许字母数字,不能使用常见的连字符分隔符。 + return normalizedPrefix + normalizedLocalOrderNo; } public static String localOrderNo(String prefix, String providerOrderNo) { - String expected = normalizedPrefix(prefix) + "-"; + String expected = normalizedPrefix(prefix); String value = Objects.toString(providerOrderNo, "").trim(); - if (!value.regionMatches(true, 0, expected, 0, expected.length()) + if (!PREFIX_PATTERN.matcher(value).matches() + || !value.startsWith(expected) || value.length() == expected.length()) { return ""; } - return value.substring(expected.length()); + String localOrderNo = value.substring(expected.length()); + // 回调只接受当前前缀后紧跟正数雪花 ID;这同时拒绝其他系统前缀、分隔符和前缀碰撞后的字母尾缀。 + return isValidLocalOrderNo(localOrderNo) ? localOrderNo : ""; } private static String normalizedPrefix(String prefix) { - String value = Objects.toString(prefix, "ASLAN").trim(); - return (value.isBlank() ? "ASLAN" : value).toUpperCase(Locale.ROOT); + String value = Objects.toString(prefix, DEFAULT_PREFIX).trim(); + String normalized = (value.isBlank() ? DEFAULT_PREFIX : value).toUpperCase(Locale.ROOT); + if (!PREFIX_PATTERN.matcher(normalized).matches()) { + // 共享商户依赖前缀隔离,配置中出现分隔符或非 ASCII 字符时必须快速失败,不能静默改写。 + throw new IllegalArgumentException("V5Pay order prefix must contain only ASCII letters and digits"); + } + return normalized; + } + + private static boolean isValidLocalOrderNo(String localOrderNo) { + if (!LOCAL_ORDER_NO_PATTERN.matcher(localOrderNo).matches()) { + return false; + } + try { + return Long.parseLong(localOrderNo) > 0; + } catch (NumberFormatException exception) { + // 最多 19 位仍可能超过 Long.MAX_VALUE;当前 IdWorker 订单不可能出现该值。 + return false; + } } } diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderStateServiceTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderStateServiceTest.java index caa4b7c9..f8f4b607 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderStateServiceTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderStateServiceTest.java @@ -174,7 +174,7 @@ public class V5PayOrderStateServiceTest { .setProductType("1263") .setStatus("2") .setFields(Map.of( - "orderNo", "ASLAN-123", + "orderNo", "ASLAN123", "amount", "14400.00", "currency", "TRY", "productType", "1263", diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignUtilsTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignUtilsTest.java index 216af0f1..54e5a398 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignUtilsTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignUtilsTest.java @@ -10,21 +10,21 @@ public class V5PaySignUtilsTest { @Test public void signSortsFieldsSkipsEmptyAndAppendsSecretDirectly() { Map fields = new LinkedHashMap<>(); - fields.put("orderNo", "ASLAN-123"); + fields.put("orderNo", "ASLAN123"); fields.put("sign", "must-be-ignored"); fields.put("merchantNo", "M"); fields.put("empty", ""); fields.put("appKey", "A"); fields.put("amount", "10.00"); - Assert.assertEquals("a092fa6ad4f5ba4829c0a8ff1986d75c", + Assert.assertEquals("dd8e6f2b76cf5107c53aff3ba1d6da5d", V5PaySignUtils.sign(fields, "secret")); } @Test public void verifyAcceptsUppercaseMd5ButRejectsChangedBusinessField() { Map fields = new LinkedHashMap<>(); - fields.put("orderNo", "ASLAN-123"); + fields.put("orderNo", "ASLAN123"); fields.put("amount", "10.00"); fields.put("sign", V5PaySignUtils.sign(fields, "secret").toUpperCase()); Assert.assertTrue(V5PaySignUtils.verify(fields, "secret")); @@ -34,10 +34,50 @@ public class V5PaySignUtilsTest { } @Test - public void orderPrefixOnlyAcceptsAslanNamespace() { - Assert.assertEquals("ASLAN-123", V5PayOrderNoUtils.providerOrderNo("aslan", "123")); - Assert.assertEquals("123", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN-123")); - Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "LALU-123")); - Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN-")); + public void orderPrefixNormalizesToUppercaseAlphanumericNamespace() { + Assert.assertEquals("ASLAN123", V5PayOrderNoUtils.providerOrderNo("aslan", "123")); + Assert.assertEquals("A5SLAN42", V5PayOrderNoUtils.providerOrderNo("a5slan", "42")); + Assert.assertEquals("ASLAN123", V5PayOrderNoUtils.providerOrderNo("", "123")); + Assert.assertEquals("123", V5PayOrderNoUtils.localOrderNo("aslan", "ASLAN123")); + } + + @Test + public void orderParserRejectsOtherNamespaceEmptyAndIllegalCharacters() { + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "LALU123")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", null)); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN-123")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN_123")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "aslan123")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN123A")); + } + + @Test + public void orderValidationRejectsValuesOutsideCurrentSnowflakeIdFormat() { + Assert.assertEquals("9223372036854775807", V5PayOrderNoUtils.localOrderNo( + "ASLAN", "ASLAN9223372036854775807")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN0")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo("ASLAN", "ASLAN0123")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo( + "ASLAN", "ASLAN9223372036854775808")); + Assert.assertEquals("", V5PayOrderNoUtils.localOrderNo( + "ASLAN", "ASLAN10000000000000000000")); + + assertInvalidOrderNo("ASLAN-", "123"); + assertInvalidOrderNo("AS LAN", "123"); + assertInvalidOrderNo("ASLAN", ""); + assertInvalidOrderNo("ASLAN", "1-23"); + assertInvalidOrderNo("ASLAN", "9223372036854775808"); + assertInvalidOrderNo("ASLAN", "10000000000000000000"); + } + + private void assertInvalidOrderNo(String prefix, String localOrderNo) { + try { + V5PayOrderNoUtils.providerOrderNo(prefix, localOrderNo); + Assert.fail("expected invalid V5Pay order number"); + } catch (IllegalArgumentException expected) { + // Invalid local/config values must fail before a signed request reaches V5Pay. + } } }