fix(order): use alphanumeric V5Pay order numbers
This commit is contained in:
parent
c70cb20de5
commit
0a4ca3df58
@ -10,11 +10,11 @@ public class PayServerNoticeControllerTest {
|
||||
@Test
|
||||
public void v5PayJsonParserPreservesSignedNumberText() throws Exception {
|
||||
Map<String, String> 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -10,21 +10,21 @@ public class V5PaySignUtilsTest {
|
||||
@Test
|
||||
public void signSortsFieldsSkipsEmptyAndAppendsSecretDirectly() {
|
||||
Map<String, Object> 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<String, Object> 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user