1
0

3 Revīzijas 92b8543625 ... 0dda1c8025

Autors SHA1 Ziņojums Datums
  alphaH 0dda1c8025 docs(汇安融对接指南): docx 重建,删建店章与门店字段 5 dienas atpakaļ
  alphaH c2ca495c1b docs(开放文档): 章节与通道名去门店,响应接口删门店键 5 dienas atpakaļ
  alphaH c7f00dbd15 feat(汇安融订单): 响应与回调不再暴露门店参数 5 dienas atpakaļ

BIN
docs/支付宝收单开放接口对接指南.docx


+ 1 - 2
frontend/src/api/module_payment/har.ts

@@ -145,8 +145,7 @@ export interface HarOrderVO {
   upstream_order_no?: string;
   platform_trade_no?: string;
   merchant_code?: string;
-  shop_code?: string;
-  third_shop_code?: string;
+  // 自 2026-10-09 起响应不含门店两键(shop_code / third_shop_code);请求侧仍可传,见 HarOrderCreateParams
   price: string;
   title?: string;
   /** alipay_stage=花呗分期 / alipay_un_stage=支付宝支付(260924 前叫 alipay_payment) */

+ 1 - 1
frontend/src/views/module_payment/apikey/collectionDoc.ts

@@ -2,7 +2,7 @@
  * 《收付款 OpenAPI 接入文档》的侧边栏条目定义(正文 `docs/collection-openapi.md`)。
  *
  * 切片逻辑在 `docSlicer.ts`,本文件只写「哪些标题拼成哪一条」。
- * 另一条通道(门店与订单)是独立产品、独立文档,见 `harDoc.ts` 与 `docs/har-openapi.md`。
+ * 另一条通道(支付宝收单)是独立产品、独立文档,见 `harDoc.ts` 与 `docs/har-openapi.md`。
  */
 import raw from "./docs/collection-openapi.md?raw";
 import { buildSections, type DocSectionDef } from "./docSlicer";

+ 1 - 1
frontend/src/views/module_payment/apikey/docSlicer.ts

@@ -3,7 +3,7 @@
  *
  * 两条通道各有一份 md、各有一组 SECTIONS,共用本文件:
  * 收付款见 `collectionDoc.ts`(`docs/collection-openapi.md`),
- * 门店与订单见 `harDoc.ts`(`docs/har-openapi.md`)。
+ * 支付宝收单见 `harDoc.ts`(`docs/har-openapi.md`)。
  *
  * md 是唯一真相,本文件不复制任何正文 —— 改 md,页面跟着变,不存在「文档改了页面没改」。
  */

+ 3 - 4
frontend/src/views/module_payment/apikey/docs/collection-openapi.md

@@ -9,7 +9,7 @@
 
 ## 1. 概述
 
-平台提供五类能力,共 11 个接口,分属**两条通道**。
+平台提供四类能力,共 10 个接口,分属**两条通道**。
 
 **收付款通道 —— 路径前缀 `/payment/openapi/pay`(详解见 5)**
 
@@ -24,11 +24,10 @@
 | 退款 | 退款查询 | `POST /payment/openapi/pay/refund/query` |
 | 收款 | 收款链接 —— 生成一条**顾客自填金额**的收款页链接(桌牌 / 菜单用) | `POST /payment/openapi/pay/checkout/link` |
 
-**门店与订单通道 —— 路径前缀 `/payment/openapi/alipay`(详解见 10)**
+**支付宝收单通道 —— 路径前缀 `/payment/openapi/alipay`(详解见 10)**
 
 | 能力 | 场景 | 接口 |
 |---|---|---|
-| 门店 | 创建门店 | `POST /payment/openapi/alipay/shop/create` |
 | 收款 | 下单收款(被扫 / 主扫由 `auth_code` 决定) | `POST /payment/openapi/alipay/order/create` |
 | 查询 | 订单查询 | `POST /payment/openapi/alipay/order/query` |
 
@@ -44,7 +43,7 @@
 |---|---|
 | `api_key` | 你的身份标识,放在 `Authorization` 头 |
 | `api_secret` | 签名密钥,**只用于本地计算签名,任何情况下都不上网传输** |
-| 商户号(`merchant_code`) | 该 `api_key` 名下可用的商户号。**一个租户可以有多个**:只有一个时什么都不用传,有多个时**下单**要用 `merchant_code` 指定用哪一个(收付款通道见 5.1;门店与订单通道见 10 章) |
+| 商户号(`merchant_code`) | 该 `api_key` 名下可用的商户号。**一个租户可以有多个**:只有一个时什么都不用传,有多个时**下单**要用 `merchant_code` 指定用哪一个(收付款通道见 5.1;支付宝收单通道见 10 章) |
 
 还需向平台方提供一个**回调地址**(`notify_url`),或在下单时逐笔传入。
 

+ 3 - 3
frontend/src/views/module_payment/apikey/docs/har-openapi.md

@@ -1,10 +1,10 @@
-## 10. 门店与订单接口
+## 10. 订单接口
 
 本章接口路径前缀 `/payment/openapi/alipay`;请求头与签名算法(3.2 / 3.3)逐字适用,错误处理总则(3.6)同样适用。
 
 **关于寻址**:本章三个接口都按**你这条 API Key 所属的租户账号**寻址,取该租户名下由平台运营报备好的商户配置。你不需要在请求里指定归属。
 
-**关于 `merchant_code`(新增,2026-09-24)**:平台**允许一个租户报备多个商户号**(例如你名下有多家门店主体,各自独立结算)。这时「这笔钱收进哪一个商户」必须由你在请求里说清楚,用 `merchant_code` 指定。
+**关于 `merchant_code`(新增,2026-09-24)**:平台**允许一个租户报备多个商户号**(例如你名下有多家经营主体,各自独立结算)。这时「这笔钱收进哪一个商户」必须由你在请求里说清楚,用 `merchant_code` 指定。
 
 - **不传** ⇒ 平台取你租户名下的商户配置。
 - **传了** ⇒ 一律以它为准,且必须是你租户名下已报备的编号,否则返回 404。
@@ -229,7 +229,7 @@ Content-Type: application/x-www-form-urlencoded
 
 ---
 
-## 11. 门店与订单通道的联调检查清单
+## 11. 订单通道的联调检查清单
 
 - [ ] 认证与签名复用 3.3 的实现,先跑通 3.4 的样例
 - [ ] `price` 用**元**、写成最简小数(不要 `1.50`、`0.010`)

+ 1 - 1
frontend/src/views/module_payment/apikey/harDoc.ts

@@ -19,7 +19,7 @@ const SECTIONS: DocSectionDef[] = [
   {
     key: "har_endpoints",
     title: "接口与联调",
-    parts: ["## 10. 门店与订单接口", "## 11. 门店与订单通道的联调检查清单"],
+    parts: ["## 10. 订单接口", "## 11. 订单通道的联调检查清单"],
   },
 ];
 

+ 1 - 1
frontend/src/views/module_payment/apikey/index.vue

@@ -1362,7 +1362,7 @@ echo "签名验证结果: " . ($isValid ? '有效' : '无效') . "\n";
               </div>
 
               <!--
-                md 切出来的条目(收付款 / 门店与订单两条通道)——
+                md 切出来的条目(银联收单 / 支付宝收单两条通道)——
                 正文由 MarkdownRenderer 渲染切片器切出的片段。
                 这里刻意不写任何接口细节:页面上的每个字段名都来自那份 md,
                 而 md 的签名样例与字段名由 Java 侧契约测试钉住。

+ 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());
     }
 }