From ac18a90da4204ec458ab118afee790fa7b3a5201 Mon Sep 17 00:00:00 2001 From: hy001 Date: Fri, 10 Jul 2026 20:39:04 +0800 Subject: [PATCH] fix(order): use alphanumeric Yumi V5Pay order numbers --- .../gateway/filter/ApiLoggingFilterTest.java | 2 +- .../V5PayWebPayPlaceAnOrderStrategy.java | 2 +- .../order/app/common/V5PayOrderNumber.java | 43 +++++++++++--- .../web/V5PayOrderSettlementServiceTest.java | 6 +- ...yServerNoticeReceivePaymentCmdExeTest.java | 8 +-- .../order/app/common/V5PayOrderDataTest.java | 22 +++---- .../app/common/V5PayOrderNumberTest.java | 59 +++++++++++++++++++ .../order/app/common/V5PaySignatureTest.java | 6 +- 8 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderNumberTest.java diff --git a/rc-gateway/src/test/java/com/red/circle/gateway/filter/ApiLoggingFilterTest.java b/rc-gateway/src/test/java/com/red/circle/gateway/filter/ApiLoggingFilterTest.java index 6b353b25..fa947bfe 100644 --- a/rc-gateway/src/test/java/com/red/circle/gateway/filter/ApiLoggingFilterTest.java +++ b/rc-gateway/src/test/java/com/red/circle/gateway/filter/ApiLoggingFilterTest.java @@ -27,7 +27,7 @@ public class ApiLoggingFilterTest { @Test public void v5PayJsonBodyPassesThroughWithoutNumericReserialization() { - byte[] raw = ("{\"orderNo\":\"YUMI-42\",\"amount\":14400.00," + byte[] raw = ("{\"orderNo\":\"YUMI42\",\"amount\":14400.00," + "\"appKey\":\"sensitive\",\"sign\":\"signature\"}") .getBytes(StandardCharsets.UTF_8); MockServerWebExchange exchange = MockServerWebExchange.from( diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/V5PayWebPayPlaceAnOrderStrategy.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/V5PayWebPayPlaceAnOrderStrategy.java index 3f3b9c13..3b644667 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/V5PayWebPayPlaceAnOrderStrategy.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/V5PayWebPayPlaceAnOrderStrategy.java @@ -37,7 +37,7 @@ public class V5PayWebPayPlaceAnOrderStrategy implements PayWebPlaceAnOrderStrate return new PlaceAnOrderResponseCO() .setTradeNo(result.getTransactionId()) - // H5 和本地状态查询始终使用无前缀本地单号,三方 YUMI- 前缀只存在于 V5Pay 协议边界. + // H5 和本地状态查询始终使用无前缀本地单号,三方 YUMI 前缀只存在于 V5Pay 协议边界. .setOrderId(details.getOrderId()) .setCurrency(details.getCurrency()) .setCountryCode(details.getCountryCode()) diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNumber.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNumber.java index 4b3b780a..03db259e 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNumber.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/V5PayOrderNumber.java @@ -1,6 +1,7 @@ package com.red.circle.order.app.common; import com.red.circle.tool.core.text.StringUtils; +import java.util.Locale; import java.util.Objects; /** @@ -12,10 +13,11 @@ public final class V5PayOrderNumber { } public static String providerOrderNo(String prefix, Long localOrderId) { - if (localOrderId == null) { + if (localOrderId == null || localOrderId <= 0) { throw new IllegalArgumentException("V5Pay local order id is missing"); } - return normalizedPrefix(prefix) + "-" + localOrderId; + // V5Pay orderNo 只接受字母数字;共享商户命名空间通过固定大写前缀直接拼接本地 Long. + return normalizedPrefix(prefix) + localOrderId; } /** @@ -23,22 +25,49 @@ public final class V5PayOrderNumber { * 不能直接截取最后一段数字,否则其他系统订单可能被错误入账到 Yumi. */ public static Long localOrderId(String prefix, String providerOrderNo) { - String expectedPrefix = normalizedPrefix(prefix) + "-"; + String expectedPrefix = normalizedPrefix(prefix); if (StringUtils.isBlank(providerOrderNo) || !providerOrderNo.startsWith(expectedPrefix)) { return null; } String local = providerOrderNo.substring(expectedPrefix.length()); - if (StringUtils.isBlank(local) || local.contains("-")) { + // Long 正数最多 19 位;先做纯 ASCII 数字和长度校验,避免符号、Unicode 数字或超长值 + // 进入宽松的数值转换后产生跨系统订单号碰撞. + if (StringUtils.isBlank(local) || local.length() > 19 || !isAsciiDigits(local)) { return null; } - return MiFaPayMysqlOrderSupport.parseOrderId(local); + Long parsed = MiFaPayMysqlOrderSupport.parseOrderId(local); + if (parsed == null || parsed <= 0 || !Objects.equals(local, parsed.toString())) { + return null; + } + return parsed; } private static String normalizedPrefix(String prefix) { - String normalized = Objects.toString(prefix, "").trim(); - if (StringUtils.isBlank(normalized) || normalized.contains("-")) { + String normalized = Objects.toString(prefix, "").trim().toUpperCase(Locale.ROOT); + if (StringUtils.isBlank(normalized) || !isAsciiLettersOrDigits(normalized)) { throw new IllegalArgumentException("V5Pay order prefix is invalid"); } return normalized; } + + private static boolean isAsciiDigits(String value) { + for (int index = 0; index < value.length(); index++) { + char item = value.charAt(index); + if (item < '0' || item > '9') { + return false; + } + } + return true; + } + + private static boolean isAsciiLettersOrDigits(String value) { + for (int index = 0; index < value.length(); index++) { + char item = value.charAt(index); + boolean letter = (item >= 'A' && item <= 'Z') || (item >= 'a' && item <= 'z'); + if (!letter && (item < '0' || item > '9')) { + return false; + } + } + return true; + } } diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderSettlementServiceTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderSettlementServiceTest.java index 738f0b3f..6bf6208a 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderSettlementServiceTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayOrderSettlementServiceTest.java @@ -104,7 +104,7 @@ public class V5PayOrderSettlementServiceTest { .setSysOrigin("LIKEI") .setUserId(100L) .setFactoryCode(WebPayProviderSupport.FACTORY_V5PAY) - .setFactoryOrderId("YUMI-42") + .setFactoryOrderId("YUMI42") .setReferenceId("2048200000000000701") .setReceiptType(InAppPurchaseReceiptType.PAYMENT.name()) .setPaymentChannel("UPI_UPI") @@ -121,12 +121,12 @@ public class V5PayOrderSettlementServiceTest { private V5PayOrderData data() { return new V5PayOrderData() - .setOrderNo("YUMI-42") + .setOrderNo("YUMI42") .setTransactionId("tx-42") .setStatus("2") .setAmount("10.00") .setCurrency("USD") .setProductType("UPI") - .setSanitizedPayload(Map.of("orderNo", "YUMI-42", "status", "2")); + .setSanitizedPayload(Map.of("orderNo", "YUMI42", "status", "2")); } } diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayServerNoticeReceivePaymentCmdExeTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayServerNoticeReceivePaymentCmdExeTest.java index dfeed761..15bfbb81 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayServerNoticeReceivePaymentCmdExeTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/command/pay/web/V5PayServerNoticeReceivePaymentCmdExeTest.java @@ -25,7 +25,7 @@ public class V5PayServerNoticeReceivePaymentCmdExeTest { public void acceptsSignedJsonWithExactDecimalAndReturnsLowercaseSuccess() { Fixture fixture = fixture(); Map fields = successFields(); - String json = "{\"orderNo\":\"YUMI-42\",\"transactionId\":\"tx-42\"," + String json = "{\"orderNo\":\"YUMI42\",\"transactionId\":\"tx-42\"," + "\"status\":2,\"amount\":10.00,\"currency\":\"USD\"," + "\"productType\":\"UPI\",\"sign\":\"" + fields.get("sign") + "\"}"; @@ -41,7 +41,7 @@ public class V5PayServerNoticeReceivePaymentCmdExeTest { fixture.order.get().setPayStatus(MiFaPayMysqlOrderSupport.PAY_STATUS_PREPAYMENT); Map wrongPrefix = successFields(); - wrongPrefix.put("orderNo", "LALU-42"); + wrongPrefix.put("orderNo", "LALU42"); wrongPrefix.put("sign", V5PaySignature.sign(wrongPrefix, "secret")); assertEquals("fail", fixture.executor.executeForm(wrongPrefix)); } @@ -81,7 +81,7 @@ public class V5PayServerNoticeReceivePaymentCmdExeTest { private Map successFields() { Map fields = new LinkedHashMap<>(); - fields.put("orderNo", "YUMI-42"); + fields.put("orderNo", "YUMI42"); fields.put("transactionId", "tx-42"); fields.put("status", "2"); fields.put("amount", "10.00"); @@ -98,7 +98,7 @@ public class V5PayServerNoticeReceivePaymentCmdExeTest { .setSysOrigin("LIKEI") .setUserId(100L) .setFactoryCode(WebPayProviderSupport.FACTORY_V5PAY) - .setFactoryOrderId("YUMI-42") + .setFactoryOrderId("YUMI42") .setReferenceId("2048200000000000701") .setReceiptType(InAppPurchaseReceiptType.PAYMENT.name()) .setPaymentChannel("UPI") diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderDataTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderDataTest.java index 4c9681c4..ea5b2192 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderDataTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderDataTest.java @@ -14,23 +14,23 @@ public class V5PayOrderDataTest { @Test public void successDataMustMatchPrefixedOrderAmountCurrencyAndProductType() { OrderUserPurchasePay order = order(); - V5PayOrderData data = data("YUMI-42", "10.00", "usd", "UPI"); + V5PayOrderData data = data("YUMI42", "10.00", "usd", "UPI"); assertTrue(data.matches(order, "YUMI")); - assertFalse(data("LALU-42", "10.00", "USD", "UPI").matches(order, "YUMI")); - assertFalse(data("YUMI-42", "10.01", "USD", "UPI").matches(order, "YUMI")); - assertFalse(data("YUMI-42", "10.00", "INR", "UPI").matches(order, "YUMI")); - assertFalse(data("YUMI-42", "10.00", "USD", "BANK_ONLINE").matches(order, "YUMI")); + assertFalse(data("LALU42", "10.00", "USD", "UPI").matches(order, "YUMI")); + assertFalse(data("YUMI42", "10.01", "USD", "UPI").matches(order, "YUMI")); + assertFalse(data("YUMI42", "10.00", "INR", "UPI").matches(order, "YUMI")); + assertFalse(data("YUMI42", "10.00", "USD", "BANK_ONLINE").matches(order, "YUMI")); } @Test public void classifiesOnlyDocumentedV5PayStatuses() { - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("2").isSuccessful()); - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("3").isFailed()); - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("5").isFailed()); - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("6").isFailed()); - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("0").isPending()); - assertTrue(data("YUMI-42", "10.00", "USD", "UPI").setStatus("1").isPending()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("2").isSuccessful()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("3").isFailed()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("5").isFailed()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("6").isFailed()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("0").isPending()); + assertTrue(data("YUMI42", "10.00", "USD", "UPI").setStatus("1").isPending()); } private OrderUserPurchasePay order() { diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderNumberTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderNumberTest.java new file mode 100644 index 00000000..5ea0901d --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PayOrderNumberTest.java @@ -0,0 +1,59 @@ +package com.red.circle.order.app.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class V5PayOrderNumberTest { + + @Test + public void generatesUppercaseAlphanumericProviderOrderNumber() { + assertEquals("YUMI42", V5PayOrderNumber.providerOrderNo(" yumi ", 42L)); + assertEquals(Long.valueOf(42L), V5PayOrderNumber.localOrderId("yumi", "YUMI42")); + assertEquals(Long.valueOf(42L), V5PayOrderNumber.localOrderId("YUMI", "YUMI42")); + } + + @Test + public void rejectsWrongPrefixEmptySuffixSeparatorsAndNonDigits() { + assertNull(V5PayOrderNumber.localOrderId("YUMI", null)); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "LALU42")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI-42")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI42A")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "yumi42")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI٤٢")); + } + + @Test + public void rejectsNonCanonicalOrOverflowingLongSuffix() { + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI0")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI00042")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI9223372036854775808")); + assertNull(V5PayOrderNumber.localOrderId("YUMI", "YUMI12345678901234567890")); + assertEquals(Long.valueOf(Long.MAX_VALUE), + V5PayOrderNumber.localOrderId("YUMI", "YUMI" + Long.MAX_VALUE)); + } + + @Test + public void rejectsInvalidPrefixAndNonPositiveLocalId() { + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("", 42L)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("YU-MI", 42L)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("YUMI_", 42L)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("余米", 42L)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("YUMI", null)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("YUMI", 0L)); + assertIllegalArgument(() -> V5PayOrderNumber.providerOrderNo("YUMI", -1L)); + } + + private void assertIllegalArgument(Runnable action) { + try { + action.run(); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + } +} diff --git a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignatureTest.java b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignatureTest.java index 52885f96..b8f4a9bd 100644 --- a/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignatureTest.java +++ b/rc-service/rc-service-order/order-application/src/test/java/com/red/circle/order/app/common/V5PaySignatureTest.java @@ -13,7 +13,7 @@ public class V5PaySignatureTest { @Test public void signsSortedNonEmptyFieldsAndAppendsSecretWithoutSeparator() { Map fields = new LinkedHashMap<>(); - fields.put("orderNo", "YUMI-123"); + fields.put("orderNo", "YUMI123"); fields.put("currency", "USD"); fields.put("merchantNo", "merchant"); fields.put("empty", ""); @@ -21,14 +21,14 @@ public class V5PaySignatureTest { fields.put("amount", "100.00"); fields.put("sign", "must-be-excluded"); - assertEquals("40e7e9f5fe267470a4448991b99e7bdd", + assertEquals("cb2ffdac30ec4a464c62d872aea2248a", V5PaySignature.sign(fields, "secret")); } @Test public void verifiesOnlyWhenSignIsPresentAndMatches() { Map fields = new LinkedHashMap<>(); - fields.put("orderNo", "YUMI-123"); + fields.put("orderNo", "YUMI123"); fields.put("status", "2"); fields.put("sign", V5PaySignature.sign(fields, "secret"));