Ver código fonte

feat(汇安融): trade_type 留空由服务端兜底 alipay_un_stage

调用方不必再传交易方式(配套:上一条提交把该字段从开放文档里拿掉)。
biz 的派生读的就是这个取值 —— 留空不兜底的话,非分期单会派生出 null,
上游收到一笔没有 biz 的交易。

- HarOrderCreateDTO.tradeType 去掉 @NotBlank:选填,留空兜底;显式传值仍原样透传、
  不校验取值(分期 alipay_stage 与后台表单走这条路)
- HarOrderService 新增 TRADE_TYPE_ALIPAY_UN_STAGE 常量与 resolveTradeType():
  供落库、precreate、pay 三条腿与 resolveBiz 派生读同一份解析结果 ——
  各读各的 dto.getTradeType() 会让留空时落库为空、上游收到默认值,对不上账
- 两条新测试先红后绿:null 走 precreate 腿(钉落库 + 上游请求 + biz 派生),
  空串走 pay 腿(钉「空串与 null 同口径」且两条腿都兜底)

后台表单、分期、收银台与收款链接腿行为不变(那几处显式传值或走别的入口)。
alphaH 5 dias atrás
pai
commit
c07a10b688

+ 4 - 4
java/src/main/java/com/payment/platform/module/payment/har/dto/HarOrderCreateDTO.java

@@ -42,12 +42,12 @@ public class HarOrderCreateDTO {
     private String title;
 
     /**
-     * alipay_stage:花呗分期  alipay_un_stage:支付宝支付。
+     * alipay_stage:花呗分期  alipay_un_stage:支付宝支付。**选填**:留空 ⇒ 平台按
+     * {@code alipay_un_stage} 填充(2026-10-09 起从调用方契约里拿掉,开放文档不再列它)。
      *
-     * <p>非分期那个值 260924 之前叫 {@code alipay_payment},规范 260924 改成
-     * {@code alipay_un_stage}。原样透传,后端不校验取值。
+     * <p>显式传值仍原样透传、后端不校验取值 —— 分期与后台表单走这条路。
+     * 非分期那个值 260924 之前叫 {@code alipay_payment}。
      */
-    @NotBlank(message = "交易方式不能为空")
     private String tradeType;
 
     /**

+ 27 - 6
java/src/main/java/com/payment/platform/module/payment/har/service/HarOrderService.java

@@ -89,6 +89,15 @@ public class HarOrderService {
      */
     private static final String BIZ_ALIPAY_UN_STAGE = "har_ali_un_stage";
 
+    /**
+     * 调用方没填 {@code trade_type} 时的默认值(非分期,支付宝支付)。
+     *
+     * <p>2026-10-09 起从调用方契约里拿掉:绝大多数接入只要非分期这一种,让每个调用方
+     * 照抄枚举值没有意义(配套:{@link HarOrderCreateDTO#getTradeType} 不再必填,
+     * 开放文档不再列它)。显式传值的仍原样透传 —— 分期与后台表单照旧。
+     */
+    private static final String TRADE_TYPE_ALIPAY_UN_STAGE = "alipay_un_stage";
+
     private final HarOrderMapper orderMapper;
     private final HarMerchantService merchantService;
     /**
@@ -473,7 +482,7 @@ public class HarOrderService {
         entity.setThirdShopCode(thirdShopCode);
         entity.setPrice(dto.getPrice());
         entity.setTitle(dto.getTitle());
-        entity.setTradeType(dto.getTradeType());
+        entity.setTradeType(resolveTradeType(dto));
         entity.setBiz(resolveBiz(dto));
         entity.setStageNum(dto.getStageNum());
         entity.setPercent(dto.getPercent());
@@ -762,16 +771,28 @@ public class HarOrderService {
                 .atZone(ZoneId.systemDefault()).toOffsetDateTime();
     }
 
+    /**
+     * 解析 trade_type(2026-10-09):留空(null / 空串)⇒ {@link #TRADE_TYPE_ALIPAY_UN_STAGE};
+     * 显式取值原样透传、不校验 —— 分期({@code alipay_stage})与后台表单走这条路。
+     *
+     * <p>落库、两条上游请求、以及 {@link #resolveBiz} 的派生都读这个解析结果:
+     * 各读各的 {@code dto.getTradeType()} 的话,留空时落库为空、上游收到默认值,对不上账。
+     */
+    private static String resolveTradeType(HarOrderCreateDTO dto) {
+        return StrUtil.isBlank(dto.getTradeType()) ? TRADE_TYPE_ALIPAY_UN_STAGE : dto.getTradeType();
+    }
+
     /**
      * 解析 biz(2026-10-09 去上游化):调用方显式给了就原样透传(后台表单与既有调用方
-     * 仍按老口径传);留空时按 trade_type 派生 —— 非分期({@code alipay_un_stage})
-     * ⇒ {@link #BIZ_ALIPAY_UN_STAGE};分期与其余取值没有规范依据,保持空(不替上游编)。
+     * 仍按老口径传);留空时按 trade_type 派生 —— 非分期({@code alipay_un_stage},
+     * 含留空被填充的那一档)⇒ {@link #BIZ_ALIPAY_UN_STAGE};分期与其余取值没有规范依据,
+     * 保持空(不替上游编)。
      */
     private static String resolveBiz(HarOrderCreateDTO dto) {
         if (StrUtil.isNotBlank(dto.getBiz())) {
             return dto.getBiz();
         }
-        return "alipay_un_stage".equals(dto.getTradeType()) ? BIZ_ALIPAY_UN_STAGE : null;
+        return TRADE_TYPE_ALIPAY_UN_STAGE.equals(resolveTradeType(dto)) ? BIZ_ALIPAY_UN_STAGE : null;
     }
 
     private HarPrecreateRequest buildPrecreateRequest(HarOrderCreateDTO dto,
@@ -782,7 +803,7 @@ public class HarOrderService {
         req.setPrice(dto.getPrice().toPlainString());
         req.setTitle(dto.getTitle());
         req.setStageNum(dto.getStageNum());
-        req.setTradeType(dto.getTradeType());
+        req.setTradeType(resolveTradeType(dto));
         req.setBiz(resolveBiz(dto));
         req.setPercent(dto.getPercent());
         req.setMerchantCode(merchant.getMerchantCode());
@@ -804,7 +825,7 @@ public class HarOrderService {
         req.setTitle(dto.getTitle());
         req.setAuthCode(dto.getAuthCode());
         req.setStageNum(dto.getStageNum());
-        req.setTradeType(dto.getTradeType());
+        req.setTradeType(resolveTradeType(dto));
         req.setBiz(resolveBiz(dto));
         req.setPercent(dto.getPercent());
         req.setMerchantCode(merchant.getMerchantCode());

+ 53 - 0
java/src/test/java/com/payment/platform/module/payment/har/service/HarOrderServiceTest.java

@@ -348,6 +348,59 @@ class HarOrderServiceTest {
                 "显式传了就不许被派生覆盖");
     }
 
+    /**
+     * {@code trade_type} 留空 ⇒ 平台按默认值 {@code alipay_un_stage} 填充(2026-10-09:
+     * 这串从调用方契约里拿掉,开放文档不再列它)。
+     *
+     * <p>填充必须同时落在**落库**与**发给上游**两处,且要早于 {@code biz} 的派生 ——
+     * 派生读的就是这个取值:空值会让非分期单派生出 null,上游收到一笔没有 biz 的交易。
+     */
+    @Test
+    void create_tradeTypeBlank_defaultsToAlipayUnStage() {
+        HarOrderCreateDTO dto = dto();
+        dto.setTradeType(null);
+        when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
+        when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
+
+        service.create(dto, "E1", "KEY1");
+
+        ArgumentCaptor<HarOrderEntity> entity = ArgumentCaptor.forClass(HarOrderEntity.class);
+        verify(orderMapper).insert(entity.capture());
+        assertEquals("alipay_un_stage", entity.getValue().getTradeType(),
+                "落库的必须是填充后的默认值");
+
+        ArgumentCaptor<HarPrecreateRequest> req = ArgumentCaptor.forClass(HarPrecreateRequest.class);
+        verify(harClient).precreate(req.capture());
+        assertEquals("alipay_un_stage", req.getValue().getTradeType(),
+                "发上游的与落库的必须是同一份");
+        assertEquals("har_ali_un_stage", req.getValue().getBiz(),
+                "默认值要参与 biz 派生 —— 留空不该让 biz 跟着变空");
+        assertEquals("har_ali_un_stage", entity.getValue().getBiz());
+    }
+
+    /**
+     * 被扫(pay)那条腿同样填默认值,且**空串与 null 同口径**。
+     *
+     * <p>两条腿各建各的请求({@code buildPrecreateRequest} / {@code buildPayRequest}),
+     * 只补一条等于漏了一条;而空串是真实存在的入参形态(调用方表单留空会发 {@code ""}),
+     * 只判 null 的实现会把它原样送给上游。
+     */
+    @Test
+    void create_tradeTypeEmptyString_scanMode_defaultsInPayRequest() {
+        HarOrderCreateDTO dto = dto();
+        dto.setTradeType("");
+        dto.setAuthCode("280812345678901234");
+        when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
+        when(harClient.pay(any())).thenReturn(response("10000", precreateDetail()));
+
+        service.create(dto, "E1", "KEY1");
+
+        ArgumentCaptor<HarPayRequest> req = ArgumentCaptor.forClass(HarPayRequest.class);
+        verify(harClient).pay(req.capture());
+        assertEquals("alipay_un_stage", req.getValue().getTradeType(),
+                "空串按留空处理,不许原样送给上游");
+    }
+
     /**
      * 上游**明确拒绝**(参数错误之类)⇒ 本地这一行必须跟着收尾。
      *