Преглед изворни кода

feat(汇安融订单): 响应与回调不再暴露门店参数

门店隐藏(2026-10-09)的报文层:门店信息不再发给租户与调用方。

- HarOrderVO 删 shop_code / third_shop_code。开放 API 三个接口、后台五个
  接口、收银台两个接口共用这一个应答体,删字段一处收口全部订单响应;
- 回调 content 不再发 third_shop_code:content 落在商户服务器与对接人的
  日志里,与订单响应同等可见;
- 挑店失败 400 文案去门店化:「该地区没有可用门店」改「该地区暂未开通
  收款」(省市码保留),这条 400 直达开放 API 调用方。

实体与库表保留两列(挑店、上游下单、对账在用);请求侧(HarOrderCreateDTO、
上游 client DTO)不动:上游下单必须带 third_shop_code。

测试先红后绿:契约测试按既有 biz 先例加 shop 键缺失断言(非空夹具 +
order_no 正对照,防空报文假绿);回调测试捕获真实表单断言 content 键集
(正控 third_order_no=HAR1);三处文案断言与一处 mock 夹具同步。
HAR 包 369/369、全量 1278/1278。
alphaH пре 5 дана
родитељ
комит
c7f00dbd15

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

@@ -12,6 +12,11 @@ import java.time.OffsetDateTime;
  * 订单应答 VO。**不含 biz**(2026-10-09 去上游化):那串取值带上游品牌,且自平台按
  * {@code trade_type} 派生后调用方既不用填也不用回读(见 {@code HarOrderService#resolveBiz})。
  * 实体与库表仍保留该列(内部实现;后台表单仍可显式传,是透传不是回读)。
+ *
+ * <p>**也不含 shop_code / third_shop_code**(2026-10-09 隐藏门店):门店不暴露给租户与
+ * 调用方(建店改由超管操作、管理页无门店栏目、开放文档不写门店字段),而本 VO 是开放
+ * API 与管理端的共用应答体 ⇒ 门店两列只留在实体与库表(挑店、上游下单、对账在用)。
+ * 契约钉在 {@code HarDtoJsonContractTest#orderVoResponse_doesNotCarryShopKeys}。
  */
 @Data
 public class HarOrderVO {
@@ -25,8 +30,6 @@ public class HarOrderVO {
     private String upstreamOrderNo;
     private String platformTradeNo;
     private String merchantCode;
-    private String shopCode;
-    private String thirdShopCode;
     private BigDecimal price;
     private String title;
     private String tradeType;
@@ -60,8 +63,6 @@ public class HarOrderVO {
         vo.setUpstreamOrderNo(e.getUpstreamOrderNo());
         vo.setPlatformTradeNo(e.getPlatformTradeNo());
         vo.setMerchantCode(e.getMerchantCode());
-        vo.setShopCode(e.getShopCode());
-        vo.setThirdShopCode(e.getThirdShopCode());
         vo.setPrice(e.getPrice());
         vo.setTitle(e.getTitle());
         vo.setTradeType(e.getTradeType());

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/har/service/HarCallbackService.java

@@ -204,7 +204,7 @@ public class HarCallbackService {
         content.put("order_no", order.getUpstreamOrderNo());
         content.put("platform_trade_no", order.getPlatformTradeNo());
         content.put("merchant_code", order.getMerchantCode());
-        content.put("third_shop_code", order.getThirdShopCode());
+        // third_shop_code 不发(2026-10-09 隐藏门店):content 落在商户服务器与对接人日志里
         content.put("price", order.getPrice());
         content.put("title", order.getTitle());
         content.put("trade_type", order.getTradeType());

+ 4 - 3
java/src/main/java/com/payment/platform/module/payment/har/service/HarShopService.java

@@ -245,9 +245,10 @@ public class HarShopService {
                         return picked;
                     }
                     if (StrUtil.isBlank(provinceCode)) {
+                        // 文案不出现「门店」(2026-10-09 隐藏门店):这条 400 直达开放 API 调用方
                         throw new BusinessException(400, cc == null
-                                ? "该地区没有可用门店: 省 " + pc
-                                : "该地区没有可用门店: " + cc + "(省 " + pc + ")");
+                                ? "该地区暂未开通收款: 省 " + pc
+                                : "该地区暂未开通收款: " + cc + "(省 " + pc + ")");
                     }
                     // 回退日志不带 IP —— IP 是个人信息,解析器自己那份日志已经掩码
                     // (口径同 HarCheckoutController#pickShopForCustomer 的 javadoc)。
@@ -264,7 +265,7 @@ public class HarShopService {
             // 挑不到、或挑中的店没有 third_shop_id ⇒ 拒。守卫同结账腿(HarCheckoutController 里那两行):
             // 静默退回「不带门店下单」等于调用方明确要求的地区归属悄悄落空。
             if (shop == null || StrUtil.isBlank(shop.getThirdShopId())) {
-                throw new BusinessException(400, "该地区没有可用门店: 省 " + pc);
+                throw new BusinessException(400, "该地区暂未开通收款: 省 " + pc);
             }
             return shop;
         }

+ 28 - 0
java/src/test/java/com/payment/platform/module/payment/har/dto/HarDtoJsonContractTest.java

@@ -120,6 +120,34 @@ class HarDtoJsonContractTest {
                 "响应体不得再出现 biz 键。实测报文: " + node);
     }
 
+    /**
+     * 订单响应体**不含 shop_code / third_shop_code 键**(2026-10-09 隐藏门店)。
+     *
+     * <p>门店整条链路不再暴露给租户与调用方:建店由超管操作、管理页无门店栏目、
+     * 开放文档不写门店字段 —— 响应体里回带门店编码,等于把刚藏起来的两格又发了出去。
+     * 实体与库表仍保留这两列(内部实现:挑店、上游下单、对账都在用)。
+     *
+     * <p>断言「键不在报文里」而不是「值为 null」(同上面 biz 那条的理由:null 仍占着键、
+     * 出现在文档与 SDK 生成器里);夹具特意给实体填上非空编码,证明钉的是 VO 的字段集。
+     * 再带一条正对照(order_no 在报文里),否则序列化成了空报文时「键缺失」照样绿。
+     */
+    @Test
+    void orderVoResponse_doesNotCarryShopKeys() {
+        HarOrderEntity e = new HarOrderEntity();
+        e.setOrderNo("AP1");
+        e.setShopCode("SH-PLATFORM-1");
+        e.setThirdShopCode("SP2026ABCDEFGHIJ");
+        HarOrderVO vo = HarOrderVO.of(e);
+
+        JsonNode node = jsonOf(vo);
+        assertEquals("AP1", node.path("order_no").asText(),
+                "正对照:报文里必须有别的字段,否则「键缺失」可能只是序列化出了空报文。实测: " + node);
+        assertTrue(node.path("shop_code").isMissingNode(),
+                "响应体不得再出现 shop_code 键。实测报文: " + node);
+        assertTrue(node.path("third_shop_code").isMissingNode(),
+                "响应体不得再出现 third_shop_code 键。实测报文: " + node);
+    }
+
     /**
      * {@code isTextual} 才是承重的那条:数值节点上调 {@code asText()} 同样拿得到
      * 正确数字串(Java 侧没丢精度,丢精度发生在浏览器),只断言 {@code asText()}

+ 33 - 0
java/src/test/java/com/payment/platform/module/payment/har/service/HarCallbackServiceTest.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.MybatisConfiguration;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.payment.platform.module.payment.apikey.entity.TenantApiKeyEntity;
 import com.payment.platform.module.payment.apikey.mapper.TenantApiKeyMapper;
 import com.payment.platform.module.payment.har.config.HarProperties;
@@ -24,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.util.MultiValueMap;
 import org.springframework.web.client.RestTemplate;
 
 import java.nio.charset.StandardCharsets;
@@ -160,6 +163,36 @@ class HarCallbackServiceTest {
         verify(publisher).changed(OrderEventPublisher.CHANNEL_HAR_ORDER, TENANT);
     }
 
+    /**
+     * 推给商户服务器的 content **不含 third_shop_code**(2026-10-09 隐藏门店)。
+     *
+     * <p>content 是回调报文,直接落进商户服务器与对接人的日志里,租户与调用方都看得见;
+     * 门店编码在去门店前是发出去的,隐藏门店后不能再发。断言抓的是**真实表单**
+     * (捕获 postForEntity 的 HttpEntity),不是读 buildContent 源码 —— 源码文本不承重。
+     */
+    @Test
+    void notifyEnterprise_contentDoesNotCarryShopCode() throws Exception {
+        stubKeyWithUrl("https://merchant.example.com/cb");
+        stubPushOk();
+        when(orderMapper.update(isNull(), any())).thenReturn(1);
+
+        HarOrderEntity o = order("K1");
+        o.setThirdShopCode("SP2026ABCDEFGHIJ");
+        service.notifyEnterprise(o);
+
+        ArgumentCaptor<HttpEntity> entity = ArgumentCaptor.forClass(HttpEntity.class);
+        verify(restTemplate, timeout(3000)).postForEntity(
+                eq("https://merchant.example.com/cb"), entity.capture(), eq(String.class));
+
+        MultiValueMap form = (MultiValueMap) entity.getValue().getBody();
+        assertNotNull(form, "推送体必须是 notify_id/timestamp/content 三格表单");
+        JsonNode content = new ObjectMapper().readTree(String.valueOf(form.getFirst("content")));
+        assertEquals("HAR1", content.path("third_order_no").asText(),
+                "正对照:回调报文本身要正常发出,其余字段都在。实测: " + content);
+        assertFalse(content.has("third_shop_code"),
+                "回调 content 不得出现 third_shop_code。实测: " + content);
+    }
+
     /**
      * 推送抛异常 ⇒ 记 FAILED,且**恰好一次 POST** —— 线程内不重试。
      *

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

@@ -571,7 +571,7 @@ class HarOrderServiceTest {
         dto.setProvinceCode("320000");
         when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
         when(shopService.pickForOrder("M001", "320000", null, "BIZ-0001"))
-                .thenThrow(new BusinessException(400, "该地区没有可用门店: 省 320000"));
+                .thenThrow(new BusinessException(400, "该地区暂未开通收款: 省 320000"));
 
         BusinessException e = assertThrows(BusinessException.class, () -> service.create(dto, "E1", "KEY1"));
 

+ 3 - 3
java/src/test/java/com/payment/platform/module/payment/har/service/HarShopRegionPickTest.java

@@ -341,7 +341,7 @@ class HarShopRegionPickTest {
         BusinessException e = assertThrows(BusinessException.class,
                 () -> service.pickForOrder("M001", null, "114.114.114.114", "BIZ-1"));
 
-        assertEquals("该地区没有可用门店: 430100(省 430000)", e.getMessage());
+        assertEquals("该地区暂未开通收款: 430100(省 430000)", e.getMessage());
     }
 
     /** 两个都没传(上线路径由 create 的守卫挡住,这里防将来直调时抛 NPE) */
@@ -415,7 +415,7 @@ class HarShopRegionPickTest {
         BusinessException e = assertThrows(BusinessException.class,
                 () -> service.pickForOrder("M001", "320000", null, "BIZ-1"));
 
-        assertEquals("该地区没有可用门店: 省 320000", e.getMessage());
+        assertEquals("该地区暂未开通收款: 省 320000", e.getMessage());
     }
 
     /**
@@ -431,6 +431,6 @@ class HarShopRegionPickTest {
         BusinessException e = assertThrows(BusinessException.class,
                 () -> service.pickForOrder("M001", "320000", null, "BIZ-1"));
 
-        assertEquals("该地区没有可用门店: 省 320000", e.getMessage());
+        assertEquals("该地区暂未开通收款: 省 320000", e.getMessage());
     }
 }