diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java index 732b1b98e..56a110c45 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java @@ -12,6 +12,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.slf4j.MDC; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; @@ -523,10 +524,13 @@ public class FinanceSettlementService { return documentNumbers.next("PAY"); } + /** 固定映射键序,避免 JVM 重启改变 Map.of 遍历顺序而误拒绝原请求。 */ private String hash(Object value) { try { + String canonical = objectMapper.writer() + .with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS).writeValueAsString(value); return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") - .digest(json(value).getBytes(StandardCharsets.UTF_8))); + .digest(canonical.getBytes(StandardCharsets.UTF_8))); } catch (Exception failure) { throw error("FIN_001", "幂等请求哈希失败"); } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementRequestHashTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementRequestHashTest.java new file mode 100644 index 000000000..e3d231d2c --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementRequestHashTest.java @@ -0,0 +1,39 @@ +package com.baiyue.finance; + +import com.baiyue.finance.service.FinanceSettlementService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceSettlementRequestHashTest { + /** 模拟不同 JVM 的 Map 遍历顺序;同一付款请求必须得到相同摘要。 */ + @Test + void equivalentRequestsIgnoreMapIterationOrderButDetectChangedPayment() { + var service = new FinanceSettlementService(null, new ObjectMapper(), null, null, null); + Map body = new LinkedHashMap<>(); + body.put("amount", 190); body.put("expectedVersion", 0); + Map first = new LinkedHashMap<>(); + first.put("path", "/settlements/202/transactions"); first.put("method", "POST"); + first.put("customerId", 10L); first.put("body", body); + Map reversedBody = new LinkedHashMap<>(); + reversedBody.put("expectedVersion", 0); reversedBody.put("amount", 190); + Map second = new LinkedHashMap<>(); + second.put("body", reversedBody); second.put("customerId", 10L); + second.put("method", "POST"); second.put("path", "/settlements/202/transactions"); + + String original = ReflectionTestUtils.invokeMethod(service, "hash", first); + String reordered = ReflectionTestUtils.invokeMethod(service, "hash", second); + assertThat(reordered).isEqualTo(original); + reversedBody.put("amount", 191); + String changed = ReflectionTestUtils.invokeMethod(service, "hash", second); + assertThat(changed).isNotEqualTo(original); + second.put("customerId", 11L); reversedBody.put("amount", 190); + String otherCustomer = ReflectionTestUtils.invokeMethod(service, "hash", second); + assertThat(otherCustomer).isNotEqualTo(original); + } +}