fix(order): use alphanumeric Yumi V5Pay order numbers
This commit is contained in:
parent
0732b6b484
commit
ac18a90da4
@ -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(
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ public class V5PayServerNoticeReceivePaymentCmdExeTest {
|
||||
public void acceptsSignedJsonWithExactDecimalAndReturnsLowercaseSuccess() {
|
||||
Fixture fixture = fixture();
|
||||
Map<String, String> 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<String, String> 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<String, String> successFields() {
|
||||
Map<String, String> 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")
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ public class V5PaySignatureTest {
|
||||
@Test
|
||||
public void signsSortedNonEmptyFieldsAndAppendsSecretWithoutSeparator() {
|
||||
Map<String, String> 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<String, String> fields = new LinkedHashMap<>();
|
||||
fields.put("orderNo", "YUMI-123");
|
||||
fields.put("orderNo", "YUMI123");
|
||||
fields.put("status", "2");
|
||||
fields.put("sign", V5PaySignature.sign(fields, "secret"));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user