|
@@ -0,0 +1,110 @@
|
|
|
|
|
+package com.payment.platform.module.payment.har.dto;
|
|
|
|
|
+
|
|
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import com.payment.platform.common.base.PaymentBaseEntity;
|
|
|
|
|
+import com.payment.platform.common.config.JacksonConfig;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 钉住汇安融三个管理端 VO 的 {@code id} 序列化形态:必须是 JSON <b>字符串</b>。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>为什么值得一个测试:{@link PaymentBaseEntity} 的 {@code id} 是
|
|
|
|
|
+ * {@code IdType.ASSIGN_ID}(雪花,19 位),超过 JS 的 {@code Number.MAX_SAFE_INTEGER}
|
|
|
|
|
+ * (9007199254740991)。一旦按 JSON number 发出去,浏览器 {@code JSON.parse} 就把它
|
|
|
|
|
+ * 折断到一个相邻的双精度值,且<b>不可逆</b>。线上实测:
|
|
|
|
|
+ * {@code 2103001449500184578} → {@code 2103001449500184600}(差 22)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>前端 {@code api/module_payment/har.ts} 把三个 VO 的 {@code id} 都声明成 {@code string},
|
|
|
|
|
+ * 并直接拼进 URL 路径({@code /shop/${id}}、{@code /order/${id}/sync}、{@code /merchant/${id}}),
|
|
|
|
|
+ * 所以后端发 number 是<b>违约</b>。症状全部指向「这条记录不存在」而不是「id 变形了」:
|
|
|
|
|
+ * 门店/订单详情 404、删商户删不掉、<b>编辑商户变成新增一条</b>(id 对不上任何行)——
|
|
|
|
|
+ * 而列表页看着完全正常,排查方向极易歪到权限或租户隔离上。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>本仓既有约定是逐字段挂 {@code ToStringSerializer}({@code LsyOrderEntity} /
|
|
|
|
|
+ * {@code LsyMerchantEntity} / {@code LsyNotifyLogEntity} / {@code LsyRefundEntity} /
|
|
|
|
|
+ * {@code BatchAuthorizeEntity})。<b>不要</b>改成在 {@link JacksonConfig} 里全局注册
|
|
|
|
|
+ * {@code Long} → {@code ToStringSerializer}:那会连 {@code total} / {@code page_size} /
|
|
|
|
|
+ * {@code retry_count} 一起变字符串,打穿全站契约。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>断言用的就是生产那个 ObjectMapper({@link JacksonConfig#objectMapper()})——
|
|
|
|
|
+ * 默认 ObjectMapper 不认这些注解,钉不住任何东西。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>与兄弟用例 {@code LsyEntityIdSerializationTest} 不同,这里<b>没有</b>「无注解的 bean 出网是
|
|
|
|
|
+ * number」那条负对照:那一条是为它「反射遍历一个实体列表」的结构准备的 —— 列表一旦为空,
|
|
|
|
|
+ * 循环体不执行、用例照样绿,所以它必须另证一遍断言不是空转。本类三个用例各自 new 一个具体的
|
|
|
|
|
+ * VO,没有那个空转模式;「id 压根没被序列化」这条路径由 {@code assertNotNull} 挡住。
|
|
|
|
|
+ */
|
|
|
|
|
+class HarDtoJsonContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ /** 与容器里那个 @Primary bean 同一个构造方式。 */
|
|
|
|
|
+ private final ObjectMapper mapper = new JacksonConfig().objectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取自线上实测:{@code pay_har_shop.id = 2103001449500184578},
|
|
|
|
|
+ * 经浏览器 JSON.parse 后变成 {@code 2103001449500184600}。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final long SNOWFLAKE_ID = 2103001449500184578L;
|
|
|
|
|
+
|
|
|
|
|
+ /** 期望的字符串形态 —— 逐位写死,不用 {@code String.valueOf(SNOWFLAKE_ID)} 现算。 */
|
|
|
|
|
+ private static final String SNOWFLAKE_ID_TEXT = "2103001449500184578";
|
|
|
|
|
+
|
|
|
|
|
+ private JsonNode jsonOf(Object bean) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return mapper.readTree(mapper.writeValueAsString(bean));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new IllegalStateException("序列化失败: " + bean.getClass().getSimpleName(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 正对照:先证明这枚针真的能扎到精度问题。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>没有这条,下面三个断言可能全在测空气 —— 比如夹具被换成一个小 id 时,
|
|
|
|
|
+ * 它们在改前改后都会绿。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void fixtureIsActuallyBeyondSafeInteger() {
|
|
|
|
|
+ assertTrue(SNOWFLAKE_ID > 9007199254740991L, "夹具必须超过 JS 安全整数上限");
|
|
|
|
|
+ assertNotEquals(SNOWFLAKE_ID, (long) (double) SNOWFLAKE_ID,
|
|
|
|
|
+ "这枚针经双精度往返后必须变成另一个值,否则它证明不了精度会丢");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void shopVoIdIsString() {
|
|
|
|
|
+ HarShopVO vo = new HarShopVO();
|
|
|
|
|
+ vo.setId(SNOWFLAKE_ID);
|
|
|
|
|
+ assertIdIsExactString(jsonOf(vo));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void orderVoIdIsString() {
|
|
|
|
|
+ HarOrderVO vo = new HarOrderVO();
|
|
|
|
|
+ vo.setId(SNOWFLAKE_ID);
|
|
|
|
|
+ assertIdIsExactString(jsonOf(vo));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void merchantVoIdIsString() {
|
|
|
|
|
+ HarMerchantVO vo = new HarMerchantVO();
|
|
|
|
|
+ vo.setId(SNOWFLAKE_ID);
|
|
|
|
|
+ assertIdIsExactString(jsonOf(vo));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * {@code isTextual} 才是承重的那条:数值节点上调 {@code asText()} 同样拿得到
|
|
|
|
|
+ * 正确数字串(Java 侧没丢精度,丢精度发生在浏览器),只断言 {@code asText()}
|
|
|
|
|
+ * 会让这个测试在改前也绿。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void assertIdIsExactString(JsonNode node) {
|
|
|
|
|
+ JsonNode id = node.get("id");
|
|
|
|
|
+ assertNotNull(id, "id 必须出现在报文里");
|
|
|
|
|
+ assertTrue(id.isTextual(), "id 必须是 JSON 字符串,实际节点类型是 " + id.getNodeType());
|
|
|
|
|
+ assertEquals(SNOWFLAKE_ID_TEXT, id.asText(), "字符串形态下必须逐位保真");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|