|
@@ -121,7 +121,16 @@ class HarOrderServiceTest {
|
|
|
assertThrows(HarApiException.class, () -> service.create(dto(), "E1", "KEY1"));
|
|
assertThrows(HarApiException.class, () -> service.create(dto(), "E1", "KEY1"));
|
|
|
|
|
|
|
|
// 记录已落库才有机会被轮询收敛
|
|
// 记录已落库才有机会被轮询收敛
|
|
|
- verify(orderMapper).insert(any());
|
|
|
|
|
|
|
+ ArgumentCaptor<HarOrderEntity> inserted = ArgumentCaptor.forClass(HarOrderEntity.class);
|
|
|
|
|
+ verify(orderMapper).insert(inserted.capture());
|
|
|
|
|
+
|
|
|
|
|
+ // ★ 超时是**未知**,不是「上游拒绝」:那笔单可能已经建好了 ⇒ 必须留在
|
|
|
|
|
+ // 「0 等待支付」继续轮询。关掉它等于把一笔可能已经收款的单写成「已关闭」,
|
|
|
|
|
+ // 而付款通知到达时会因为本地已是终态而被幂等跳过 —— 钱在,账没了。
|
|
|
|
|
+ assertEquals(HarOrderStatus.WAIT_PAY, inserted.getValue().getTradeStatus());
|
|
|
|
|
+ assertNotNull(inserted.getValue().getNextQueryTime(),
|
|
|
|
|
+ "超时的单必须留在轮询里,否则永远没人去问结果");
|
|
|
|
|
+ verify(orderMapper, never()).update(isNull(), any());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -224,8 +233,24 @@ class HarOrderServiceTest {
|
|
|
assertEquals("har_ali_un_stage", req.getValue().getBiz());
|
|
assertEquals("har_ali_un_stage", req.getValue().getBiz());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上游**明确拒绝**(参数错误之类)⇒ 本地这一行必须跟着收尾。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用户在联调里报的现象:这种单停在「0 等待支付」,而轮询按 PENDING={0,4} 捞单,
|
|
|
|
|
+ * 于是每个周期都去上游问一笔**在上游从来没被建出来**的订单,日志里一直刷
|
|
|
|
|
+ * 「找不到订单」,永远问不出结果。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 收尾两件事:
|
|
|
|
|
+ * ① `trade_status` 改「8 交易关闭」—— 8 不在 PENDING 里,捞单条件立刻失效;
|
|
|
|
|
+ * ② `next_query_time` 写回 **null**。
|
|
|
|
|
+ * ★ 这一格只能走 `update(null, UpdateWrapper.set(...))`:MP 默认的 NOT_NULL 策略
|
|
|
|
|
+ * 会把实体里的 null 字段整个跳过 ⇒ `setNextQueryTime(null)` + `updateById`
|
|
|
|
|
+ * 是**静默无效**的(本仓转账模块踩过同款:setNextRetryAt(null) 被吞)。
|
|
|
|
|
+ * 只做 ① 也能挡住捞单(两个条件是与),但留着一个非 null 的 next_query_time
|
|
|
|
|
+ * 等于把「这单结束了」只表达了一半。
|
|
|
|
|
+ */
|
|
|
@Test
|
|
@Test
|
|
|
- void create_upstreamNonSuccessCode_throws() {
|
|
|
|
|
|
|
+ void create_upstreamNonSuccessCode_throwsAndClosesTheLocalRow() {
|
|
|
when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
when(harClient.precreate(any())).thenReturn(response("40004", null));
|
|
when(harClient.precreate(any())).thenReturn(response("40004", null));
|
|
|
|
|
|
|
@@ -234,6 +259,27 @@ class HarOrderServiceTest {
|
|
|
|
|
|
|
|
assertTrue(e.getMessage().contains("下单失败"));
|
|
assertTrue(e.getMessage().contains("下单失败"));
|
|
|
verify(orderMapper).insert(any());
|
|
verify(orderMapper).insert(any());
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<UpdateWrapper> w = ArgumentCaptor.forClass(UpdateWrapper.class);
|
|
|
|
|
+ verify(orderMapper).update(isNull(), w.capture());
|
|
|
|
|
+ UpdateWrapper<?> closing = w.getValue();
|
|
|
|
|
+
|
|
|
|
|
+ // 断言一律走 setValueOf(只看 SET 段):WHERE 里的 id 在这一刻也是 null
|
|
|
|
|
+ // (这一行是刚 insert 的,mock 的 mapper 不会回填主键)⇒ 倒 paramNameValuePairs
|
|
|
|
|
+ // 全集去 containsValue(null) 会被 WHERE 那一格**假绿**。
|
|
|
|
|
+ assertEquals(HarOrderStatus.CLOSED, setValueOf(closing, "trade_status"),
|
|
|
|
|
+ "必须落到「8 交易关闭」,否则它还在 PENDING 里、每轮都被捞去问上游。实测 SET: "
|
|
|
|
|
+ + closing.getSqlSet());
|
|
|
|
|
+ assertNull(setValueOf(closing, "next_query_time"),
|
|
|
|
|
+ "next_query_time 要写回 null(实体的 null 会被 MP 的 NOT_NULL 策略跳过,"
|
|
|
|
|
+ + "所以只能走 SET 子句)。实测 SET: " + closing.getSqlSet());
|
|
|
|
|
+ assertEquals("上游返回 40004: msg", setValueOf(closing, "description"),
|
|
|
|
|
+ "上游那句拒绝原因要留在本地,页面上才看得出为什么没收单。实测 SET: "
|
|
|
|
|
+ + closing.getSqlSet());
|
|
|
|
|
+ // WHERE 只钉「有没有」,不钉值,理由同上(此刻 id 还是 null)。
|
|
|
|
|
+ // 漏掉 WHERE 就是**更新全表**,所以这一条必须留着。
|
|
|
|
|
+ assertTrue(closing.getSqlSegment().contains("id"),
|
|
|
|
|
+ "少了 WHERE 就是更新全表。实测: " + closing.getSqlSegment());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
@@ -727,6 +773,29 @@ class HarOrderServiceTest {
|
|
|
+ patch.getValue().getRefundReason());
|
|
+ patch.getValue().getRefundReason());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取 SET 子句里某一列**绑定**的值 —— 只看 SET,不看 WHERE。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么不直接 `w.getParamNameValuePairs().containsValue(x)`:那张表是 SET 与 WHERE
|
|
|
|
|
+ * 共用的,两段混在一起时一条「contains」可能被**另一段**的绑定值满足。本仓有现成的
|
|
|
|
|
+ * 踩坑形状:`update(null, wrapper).eq("id", 刚insert还没主键的实体.getId())` 里 id 绑的
|
|
|
|
|
+ * 就是 null,于是「containsValue(null) 证明 next_query_time 被写成 null」是假绿。
|
|
|
|
|
+ * 占位符形如 `next_query_time=#{ew.paramNameValuePairs.MPGENVAL3}`,从 SET 串里取回
|
|
|
|
|
+ * 那个 key 再查值,归属就唯一了。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Object setValueOf(UpdateWrapper<?> w, String column) {
|
|
|
|
|
+ for (String item : w.getSqlSet().split(",")) {
|
|
|
|
|
+ if (!item.startsWith(column + "=")) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Matcher m = Pattern.compile("paramNameValuePairs\\.(\\w+)").matcher(item);
|
|
|
|
|
+ assertTrue(m.find(), "这一列没有绑定值,形状不认识: " + item);
|
|
|
|
|
+ return w.getParamNameValuePairs().get(m.group(1));
|
|
|
|
|
+ }
|
|
|
|
|
+ fail("SET 子句里没有这一列: " + column + ",实测 SET: " + w.getSqlSet());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 只取 WHERE 段引用到的绑定值,不倒 `paramNameValuePairs` 全集 —— 后者会把 SET 的绑定值也算进来 */
|
|
/** 只取 WHERE 段引用到的绑定值,不倒 `paramNameValuePairs` 全集 —— 后者会把 SET 的绑定值也算进来 */
|
|
|
private static Set<Object> whereBoundValues(UpdateWrapper<?> w) {
|
|
private static Set<Object> whereBoundValues(UpdateWrapper<?> w) {
|
|
|
Matcher m = Pattern.compile("paramNameValuePairs\\.([A-Za-z0-9_]+)")
|
|
Matcher m = Pattern.compile("paramNameValuePairs\\.([A-Za-z0-9_]+)")
|