fix(finance): stabilize settlement idempotency request hashes
This commit is contained in:
@@ -12,6 +12,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest;
|
|||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import org.slf4j.MDC;
|
import org.slf4j.MDC;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||||
@@ -523,10 +524,13 @@ public class FinanceSettlementService {
|
|||||||
return documentNumbers.next("PAY");
|
return documentNumbers.next("PAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 固定映射键序,避免 JVM 重启改变 Map.of 遍历顺序而误拒绝原请求。 */
|
||||||
private String hash(Object value) {
|
private String hash(Object value) {
|
||||||
try {
|
try {
|
||||||
|
String canonical = objectMapper.writer()
|
||||||
|
.with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS).writeValueAsString(value);
|
||||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
.digest(json(value).getBytes(StandardCharsets.UTF_8)));
|
.digest(canonical.getBytes(StandardCharsets.UTF_8)));
|
||||||
} catch (Exception failure) {
|
} catch (Exception failure) {
|
||||||
throw error("FIN_001", "幂等请求哈希失败");
|
throw error("FIN_001", "幂等请求哈希失败");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("amount", 190); body.put("expectedVersion", 0);
|
||||||
|
Map<String, Object> first = new LinkedHashMap<>();
|
||||||
|
first.put("path", "/settlements/202/transactions"); first.put("method", "POST");
|
||||||
|
first.put("customerId", 10L); first.put("body", body);
|
||||||
|
Map<String, Object> reversedBody = new LinkedHashMap<>();
|
||||||
|
reversedBody.put("expectedVersion", 0); reversedBody.put("amount", 190);
|
||||||
|
Map<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user