|
|
@@ -1,8 +1,10 @@
|
|
|
package com.payment.platform.module.payment.leshouying.service;
|
|
|
|
|
|
import cn.hutool.core.util.IdUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
+import com.payment.platform.module.payment.leshouying.client.LeshouyingApiException;
|
|
|
import com.payment.platform.module.payment.leshouying.client.LeshouyingClient;
|
|
|
import com.payment.platform.module.payment.leshouying.client.LeshouyingResponseParser;
|
|
|
import com.payment.platform.module.payment.leshouying.config.LeshouyingProperties;
|
|
|
@@ -394,17 +396,27 @@ public class LsyRefundService {
|
|
|
public Map<String, Object> queryRefund(LsyRefundEntity refund) {
|
|
|
Map<String, Object> p = new LinkedHashMap<>();
|
|
|
// ★ mch_no 是**必填**(上游文档「退款查询」入参表),而 pay_lsy_refund 没有这一列
|
|
|
- // ⇒ 只能顺着 order_id 回订单表取。判据不是「文档这么写」,是**结构孪生方法**:
|
|
|
+ // ⇒ 只能回订单表取。判据不是「文档这么写」,是**结构孪生方法**:
|
|
|
// LsyQueryService.queryOrder 形状完全平行(同样的 inst_no/mch_no/query_trace_no/
|
|
|
// 二选一定位键)且送了 mch_no、联调实测查单走通;createRefund 也送。缺它的只有本方法。
|
|
|
- // 取不到就**不送这个键**(而不是送 null):LeshouyingClient.post 会过滤副本里的 null 值,
|
|
|
- // 所以两者在上游看来一样,但「送了个 null 键」会让钉 keySet 的用例失去覆盖
|
|
|
- // ⇒ 反查失败这条路永远测不到。宁可退化为改动前的行为,也不静默留个空键。
|
|
|
- LsyOrderEntity order = refund.getOrderId() == null ? null
|
|
|
- : orderMapper.selectById(refund.getOrderId());
|
|
|
- if (order != null && order.getMchNo() != null) {
|
|
|
- p.put("mch_no", order.getMchNo());
|
|
|
+ //
|
|
|
+ // ★★ 2026-09-30 修:取不到时**不再把请求发出去**。原写法是「取不到就不送这个键」,
|
|
|
+ // 而那是**注定被拒**的一跳:上游对缺必填字段的应答是「有必传字段未传!」——
|
|
|
+ // 一句把读日志的人引向「参数拼错了」的话,而真因是**本地取不到订单**(另一回事)。
|
|
|
+ // 现在改成在本地就把话说清楚,且不花那一次网络往返。
|
|
|
+ // 两条腿的降级行为**各自保持原样**:开放 API 那条腿的
|
|
|
+ // `queryUpstreamQuietly` 本来就只接 LeshouyingApiException ⇒ 照旧降级成
|
|
|
+ // 「返回本地状态、detail 为 null」(文档 §3.6 的 UNKNOWN 就是为这种情形写的);
|
|
|
+ // 对账调度器那条腿逐行 try/catch,会把这条记成一次带栈的 ERROR(那正是它该有的响度)。
|
|
|
+ String mchNo = resolveMchNo(refund);
|
|
|
+ if (StrUtil.isBlank(mchNo)) {
|
|
|
+ throw new LeshouyingApiException("LOCAL_NO_MCH_NO",
|
|
|
+ "退款查询缺少商户号:按 orderId/outTradeNo 都找不到订单记录"
|
|
|
+ + "(orderId=" + refund.getOrderId()
|
|
|
+ + ", outTradeNo=" + LsyLogSanitizer.flatten(refund.getOutTradeNo()) + ")",
|
|
|
+ null);
|
|
|
}
|
|
|
+ p.put("mch_no", mchNo);
|
|
|
p.put("query_trace_no", IdUtil.fastSimpleUUID());
|
|
|
if (refund.getRefundNo() != null) {
|
|
|
p.put("refund_no", refund.getRefundNo());
|
|
|
@@ -415,6 +427,38 @@ public class LsyRefundService {
|
|
|
return client.post("/v3/queryrefund", p);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 取这笔退款的商户号:先按 {@code order_id} 直取,取不到再按 {@code out_trade_no} 兜一次。
|
|
|
+ *
|
|
|
+ * <p>【为什么要第二条路】{@code pay_lsy_refund} 同时有 {@code order_id} 与
|
|
|
+ * {@code out_trade_no} 两列,而它们**不是同一份信息的两种写法**:前者是本库订单行的主键,
|
|
|
+ * 后者是业务单号。历史行(预插入那条腿上线之前建的)的 {@code order_id} 可能是空的,
|
|
|
+ * 而 {@code out_trade_no} 一直在 —— 只认前者会让这类行永远查不了退款状态,
|
|
|
+ * 且失败形态是**上游那句误导人的「有必传字段未传!」**(见 {@link #queryRefund} 的注释)。
|
|
|
+ *
|
|
|
+ * <p>【为什么带 tenant_id】这条腿的调用方里有**未经认证**的对账调度器
|
|
|
+ * ({@code LsyReconcileScheduler}),而 {@code TenantInnerInterceptor} 只对已认证请求
|
|
|
+ * 追加租户谓词 ⇒ 调度器跑的时候那条谓词不存在,只有本方法自己带上,
|
|
|
+ * 才不会跨租户取到一个同号订单。{@code out_trade_no} 在**租户内**唯一,
|
|
|
+ * 跨租户不保证,所以这一条不是可省的谨慎。
|
|
|
+ *
|
|
|
+ * @return 商户号;{@code null} = 两条路都没定位到订单(调用方据此拒发请求)
|
|
|
+ */
|
|
|
+ private String resolveMchNo(LsyRefundEntity refund) {
|
|
|
+ LsyOrderEntity order = refund.getOrderId() == null ? null
|
|
|
+ : orderMapper.selectById(refund.getOrderId());
|
|
|
+ if (order == null && StrUtil.isNotBlank(refund.getOutTradeNo())) {
|
|
|
+ LambdaQueryWrapper<LsyOrderEntity> w = new LambdaQueryWrapper<>();
|
|
|
+ w.eq(LsyOrderEntity::getOutTradeNo, refund.getOutTradeNo())
|
|
|
+ .eq(refund.getTenantId() != null, LsyOrderEntity::getTenantId, refund.getTenantId())
|
|
|
+ // 不加 LIMIT 时 selectOne 命中多行会抛异常;这里要的是「取一条来拿商户号」,
|
|
|
+ // 多行本身就是不该出现的状态,用 LIMIT 1 让它退化成可用而不是崩掉。
|
|
|
+ .last("LIMIT 1");
|
|
|
+ order = orderMapper.selectOne(w);
|
|
|
+ }
|
|
|
+ return order == null ? null : order.getMchNo();
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 退款查询结果收敛 —— 退款对账腿({@code LsyReconcileScheduler.reconcilePendingRefunds})的解释器。
|
|
|
*
|