fix(order): use alphanumeric V5Pay order numbers

This commit is contained in:
zhx 2026-07-10 20:39:26 +08:00
parent c70cb20de5
commit 0a4ca3df58
4 changed files with 89 additions and 17 deletions

View File

@ -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"));
}
}

View File

@ -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;
}
}
}

View File

@ -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",

View File

@ -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.
}
}
}