|
|
@@ -251,4 +251,62 @@ class LeshouyingResponseParserTest {
|
|
|
assertNull(ex.getCause(), "空报文没进过 JSON 解析,不该有 cause");
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ <b>真实的 {@code PAY_IN_PROCESS} 应答</b> —— 2026-09-28 打测试环境拿到的,逐字抄录。
|
|
|
+ *
|
|
|
+ * <p>来源:给一笔真实的 H5 单({@code /v3/h5pay} 回了 {@code PAY_SUCCESS} + {@code trade_no})
|
|
|
+ * 按 {@code trade_no} 查 {@code /v3/queryorder},上游回了 614 字节 / 24 字段。
|
|
|
+ * 应答自带的 {@code sign} 已用 {@code LeshouyingSigner.verify} 复算通过(null 排除读法)
|
|
|
+ * ⇒ 这份 JSON 是**上游自己发的**,不是我构造的。
|
|
|
+ *
|
|
|
+ * <p>★ <b>为什么必须有一条真货向量</b>:本类此前对 PAY_IN_PROCESS 只有一条**两字段**的合成报文
|
|
|
+ * ({@link #parseV3_payInProcess_isNotSuccessButNotException})。合成报文答不了
|
|
|
+ * 「上游真发的 PAY_IN_PROCESS 长什么样」—— {@code trade_state}、{@code receipt_amount}
|
|
|
+ * 这些字段会不会出现、出现成什么值,只有真货知道。
|
|
|
+ *
|
|
|
+ * <p>★ <b>它同时要钉住文档那句「判断交易结果以 {@code result_code} 为准,{@code trade_state}
|
|
|
+ * 仅供参考」</b>。但注意:真报文里 {@code trade_state=USERPAYING} 与
|
|
|
+ * {@code result_code=PAY_IN_PROCESS} **恰好同向** ⇒ 光看这一条**区分不了**「实现看的是哪个」。
|
|
|
+ * 真正钉住它的是下面那句反向守卫(把两者拆开)。没有那句,本用例在「实现其实读 trade_state」
|
|
|
+ * 下照样全绿。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void parseV3_realPayInProcessResponse_resultCodeBeatsTradeState() {
|
|
|
+ // 逐字抄自 logs/_lsy-query-by-tradeno-resp.bin(614 字节;字段顺序照原文,未重排)
|
|
|
+ String real = "{\"return_code\":\"SUCCESS\",\"return_msg\":\"用户支付中\","
|
|
|
+ + "\"result_code\":\"PAY_IN_PROCESS\",\"pay_type\":\"300\","
|
|
|
+ + "\"mch_name\":\"建行小店基本休息修改\",\"shop_name\":\"\","
|
|
|
+ + "\"mch_no\":\"Y52211090004\",\"shop_no\":\"\","
|
|
|
+ + "\"query_trace_no\":\"483a97c660a14921942751ba3ad4af79\","
|
|
|
+ + "\"pay_trace_no\":\"probe20260928160450h5\",\"pay_time\":\"20260928160450\","
|
|
|
+ + "\"total_amount\":\"1\",\"end_time\":null,"
|
|
|
+ + "\"trade_no\":\"200001070X4326092816045010002\",\"td_trade_no\":null,"
|
|
|
+ + "\"qd_trade_no\":null,\"user_id\":null,\"attach\":null,"
|
|
|
+ + "\"trade_state\":\"USERPAYING\",\"receipt_amount\":\"0\",\"pay_amount\":\"1\","
|
|
|
+ + "\"coupon_amount\":null,\"discount_amount\":\"0\","
|
|
|
+ + "\"sign\":\"18146ea94b94f4264992243d90d199be\"}";
|
|
|
+
|
|
|
+ Map<String, Object> r = parser.parseV3(real);
|
|
|
+ assertEquals("PAY_IN_PROCESS", r.get("result_code"));
|
|
|
+ assertTrue(parser.isPayInProcess(r), "上游真实应答必须是「支付中」");
|
|
|
+ assertFalse(parser.isPaySuccess(r), "支付中不得被判成成功 —— 那会提前写死一个终态");
|
|
|
+ assertFalse(parser.isPayFail(r), "支付中不得被判成失败 —— 那正是本类要防的方向");
|
|
|
+
|
|
|
+ // ★ 真实陷阱:这一单一分钱都还没收到(用户连支付页都没打开),
|
|
|
+ // 而 receipt_amount / discount_amount 都是**非 null 的 "0"**。
|
|
|
+ assertEquals("0", r.get("receipt_amount"));
|
|
|
+ assertEquals("0", r.get("discount_amount"));
|
|
|
+ assertNotNull(r.get("receipt_amount"), "是零,不是 null —— 别写「非 null 即已收款」这种判据");
|
|
|
+
|
|
|
+ // ★ 反向守卫:把 trade_state 改成与 result_code **相反**的说法 ⇒ 判定必须仍跟随 result_code。
|
|
|
+ String contradictoryJson = real.replace(
|
|
|
+ "\"trade_state\":\"USERPAYING\"", "\"trade_state\":\"SUCCESS\"");
|
|
|
+ assertNotEquals(real, contradictoryJson,
|
|
|
+ "正对照:替换必须真的发生,否则下面两句是自证的(搜索串写错就静默失效)");
|
|
|
+ Map<String, Object> contradictory = parser.parseV3(contradictoryJson);
|
|
|
+ assertFalse(parser.isPaySuccess(contradictory),
|
|
|
+ "trade_state 说成功、result_code 说支付中 ⇒ 必须听 result_code(文档明说 trade_state 仅供参考)");
|
|
|
+ assertTrue(parser.isPayInProcess(contradictory));
|
|
|
+ }
|
|
|
}
|