Jelajahi Sumber

fix(乐收赢): 两处 404 文案不再把租户指去一个它进不去的页面

起因是本批「商户号改成超管代录」那个提交(5db0155b)留下的一处自相矛盾:
`LsyMerchantService` 的两个 404 出口仍写着「请先在「手工登记商户号」中登记」,
而那一页的写入口在同一个提交里已经收成**超管身份** —— 租户看不到新增按钮、端点也 403。
同一句文案,改动前是可照做的指引,改动后是一条**照做不了的死路**:
操作员会照着它去那一页找一个不存在的按钮。

前端同批交付的只读提示早就说了正确的话
(`collection/merchant/index.vue`:「商户号由平台运营录入,如需新增或修改请联系平台运营」),
后端这两句没跟上。

## 改了什么

两处出口(`requireByEnterprise` / `requireByTenant`)的文字
「请先在「手工登记商户号」中登记」→「请联系平台运营登记」。
企业级那句仍带 `enterpriseId`,那是有用的排障信息,没动。

## 验证读数

- 先红后绿:改前跑 `LsyMerchantServiceTest` 是 43 跑 2 败,两条新用例各自报出当时的
  实际文案(`实测文案: 该租户未配置商户号,请先在「手工登记商户号」中登记`);
  改后 43 全绿。
- ★ 一条断言**从没见过它红**就不算验过:两条新用例各是一对断言
  (`assertTrue(含「联系平台运营」)` + `assertFalse(含「手工登记商户号」)`),
  而首轮红是在前一句就中断的 ⇒ `assertFalse` 那半句当时**根本没被执行过**。
  补了一次变异:把租户那句改成
  `…请联系平台运营登记,也可先在「手工登记商户号」中登记` —— 前半句成立、后半句仍违规。
  读数:红,落在 `assertFalse` 那一行(当时实测 `LsyMerchantServiceTest.java:179`),
  且**只**红租户这一条(企业级那句没动,仍绿)⇒ 两条断言各自带电。
  还原后与基线 `cp` 副本逐字节相同(同一工作区形态下 sha256 均为 `1d4c8f0a…`),
  变异体字面量在还原后命中数 0。
- 全量 942 绿(此前 940;+2 就是本次两条新用例)。既有的
  `contains("未配置商户号")` 断言不受影响 —— 新旧文案都含这四个字。
  没有任何其它用例钉这两句话,这一点是上面那次全量跑出来的,不是推断。

## 顺带订正三处被本次改动写旧的交叉引用

都不是新增文案,是**别的文件里指向这段文案的坐标**,不改就会在下次被当事实引用:

- `lsy-tenant-page-grants.sql`:§3 原本给的理由是「界面文案要求租户去那一页…
  租户没有这一页时这句话是一条死路」。**那条理由随本次改动消失**,
  而结论(该页要授给租户)仍成立,依据换成「它是租户自查自己商户号的只读视图」。
  该文件**尚未执行**,交付理由错了会直接影响用户的执行判断,故必须改,不能只留痕。
  同一段里另一处一并订正:原文写「新增时归属强制取自认证上下文,
  `save` 里 `entity.setTenantId(null)`」—— 那行已在 5db0155b 删除,现在归属由超管
  显式指定、新增时必填。写入口收成身份门禁后,原文那个
  「把 `CollectionMerchant` 从 `pages` 里删掉再执行」的裁量口子也随之关闭。
- `lsy-merchant-menu-rename.sql`:它有一张「同步改过的源码位置(供核对)」清单。
  **没有**改那两行历史快照(那是 2026-09-22 那次改名时的事实,改了就是伪造历史),
  只在清单下加一条 2026-09-30 追记,说明这两条文案已不在、
  这份清单不能拿去核对当前源码。
- `LsyOnboardingService` 的 javadoc 引用了 `requireByTenant` 那句 404 原文,跟着改。

## 没做什么

- 没碰 `LeshouyingMerchantController` 的门禁(那是上一个提交的事)。
- 没改前端:`merchant/index.vue` 那句只读提示本来就是对的。
- 没有重新部署 —— 本条与同批 LSY 改动一样,**要重新部署才生效**。
alphaH 2 minggu lalu
induk
melakukan
61b6201f9e

+ 10 - 2
java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyMerchantService.java

@@ -52,8 +52,15 @@ public class LsyMerchantService {
                 .eq(LsyMerchantEntity::getEnterpriseId, enterpriseId)
                 .last("LIMIT 1"));
         if (m == null) {
+            // ★ 出口文案(2026-09-30):**不再**指向「手工登记商户号」。
+            //   录入已从「租户自助」改成**超管代录**(LeshouyingMerchantController 的
+            //   save/delete 挂 @perm.hasRole('SUPERUSER')),租户看不到按钮、端点也 403 ——
+            //   旧文案会把调用方指去一个它自己进不去的地方,是一条照做不了的死路。
+            //   这里服务的是本方法的两类读者:租户(照做的动作只有「联系平台运营」)
+            //   与超管(读到自己该去录)。回归:LsyMerchantServiceTest 两条
+            //   `…_message_routesToPlatformOps_…`(正负两侧都断言)。
             throw new BusinessException(404,
-                    "该企业未配置商户号,请先在「手工登记商户号」中登记: " + enterpriseId);
+                    "该企业未配置商户号,请联系平台运营登记: " + enterpriseId);
         }
         return m;
     }
@@ -81,8 +88,9 @@ public class LsyMerchantService {
                 .isNull(LsyMerchantEntity::getEnterpriseId)
                 .last("LIMIT 1"));
         if (m == null) {
+            // 同 requireByEnterprise:出口文案不再指向对租户只读的那一页(2026-09-30)。
             throw new BusinessException(404,
-                    "该租户未配置商户号,请先在「手工登记商户号」中登记");
+                    "该租户未配置商户号,请联系平台运营登记");
         }
         return m;
     }

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingService.java

@@ -41,7 +41,7 @@ import java.util.Set;
  *   <li><b>不查本地映射是有意的,不是漏了。</b>进件阶段这个商户<b>还没进
  *       {@code pay_lsy_merchant}</b>(那一行是进件成功之后才写的)—— 所以在这里调
  *       {@code LsyMerchantService.requireByTenant} / {@code requireByMchNo} 必然落空,
- *       报的是它们各自那句 404({@code 该租户未配置商户号,请先在「手工登记商户号」中登记} /
+ *       报的是它们各自那句 404({@code 该租户未配置商户号,请联系平台运营登记} /
  *       {@code "商户号不存在: " + mchNo})。那正是本轮要防的「校验前提不成立」:**在进件阶段,
  *       『配置里没有这个商户』不是异常,而是这个阶段的常态** —— 一句 404 会把操作者
  *       送去配一个此刻不可能存在的映射。</li>

+ 5 - 0
java/src/main/resources/db/lsy-merchant-menu-rename.sql

@@ -33,6 +33,11 @@
 --       merchant/index.vue:7        页面顶部说明
 --       merchant/index.vue:69       弹窗标题(编辑/新增)
 --       merchant/index.vue:239      删除确认文案
+--     ★ 2026-09-30 追记:上面那**两条 404 文案已经不在了** —— 同日「商户号改成超管代录」
+--       之后,它们改成了「请联系平台运营登记」(`LsyMerchantService` 的
+--       requireByTenant / requireByEnterprise),不再指名任何菜单。
+--       **不要拿这份清单去核对当前源码**:它是 2026-09-22 那次改名的快照。
+--       (菜单名「手工登记商户号」本身没再变,`merchant/index.vue` 三处仍用它。)
 --
 -- ── 定位方式 ────────────────────────────────────────────────────────────────
 -- 用 `route_name` 定位而不是 `name`:`name` 正是要被改掉的那一列,

+ 20 - 16
java/src/main/resources/db/lsy-tenant-page-grants.sql

@@ -24,28 +24,32 @@
 --        「角色详情不带授权 ⇒ 抽屉全不勾且保存清空授权」那个缺陷(`1bda59a`,2026-09-28 才修)
 --        期间被谁点过一次保存。两种都不改变本脚本要恢复的目标形态。
 --
--- 3) 界面文案**要求**租户去那一页:收银台在没有商户号时抛
---      「该租户未配置商户号,请先在「手工登记商户号」中登记」
---      (`LsyMerchantService.requireByTenant`)。而「手工登记商户号」正是
---      `route_name = 'CollectionMerchant'` 那一页改名后的名字。
---      ⇒ 租户没有这一页时,这句话是**一条死路**:它指的地方租户进不去。
+-- 3) 租户需要这一页来自查「我配的是哪个商户号」。
+--      ★ 2026-09-30 订正:本条 2026-09-28 写的理由是「界面文案**要求**租户去那一页
+--        (`LsyMerchantService.requireByTenant` 那句 404 指名「手工登记商户号」),
+--        租户没有这一页时这句话是**一条死路**」。**那条理由已不成立** ——
+--        同一批改动把那两处 404 的文案从「请先在「手工登记商户号」中登记」
+--        改成了「请联系平台运营登记」(`requireByTenant` / `requireByEnterprise`),
+--        不再指名任何页面 ⇒ 租户看不到这一页也不会被指去一个进不去的地方。
+--        结论不变,依据换一条:该页对租户是**只读**的自身商户号视图,
+--        正是「钱收了但下单报未配置商户号」时最该先看一眼的地方。
 --
 -- ── ★ 你可以否掉哪一条 ────────────────────────────────────────────────────
 --
 -- 前两页(支付订单 / 退款记录)是**纯读**:控制器只有 `GET /page` 与 `GET /{...}`,
 -- 没有写端点,租户看到的是自己租户的行(实体继承 PaymentTenantBaseEntity,租户拦截器追加条件)。
 --
--- **第三页(商户映射)是唯一有写权限的**,所以单独说明,你可以只删这一段:
---   · 该页三个端点(page / save / delete)**共用同一个权限串**
---     `module_payment:leshouying:merchant:list` ⇒ 授了页面就等于授了新增与删除;
---   · 新增时归属被强制取自认证上下文(`LeshouyingMerchantController.save` 里
---     `entity.setTenantId(null)`),请求体里写 tenant_id 无效;
---   · `uk_lsy_merchant_mch_no` 是 **mch_no 全局唯一**索引 ⇒ 别人已登记的商户号抢不走。
---   · 残余风险(我认为可接受,但这是判断):租户可以登记一个**尚无人登记**的商户号。
---     最坏后果是让那个商户号的账户收到本租户顾客的钱(钱进的是对方账户),
---     以及**占位**(该号之后别人登记不了,需管理员 delete 释放)。
---   · 若你不接受这一点:把本文件里 `CollectionMerchant` 那一行从 `pages` 的 VALUES 中删掉再执行,
---     并把上面第 3 条的文案改成「请联系平台管理员开通」。
+-- **第三页(商户映射)曾经是唯一有写权限的**,所以单独列了一段风险说明。
+--   ★ 2026-09-30:**那段风险已经不存在了**,本页现在和另外两页一样是只读。
+--     同一批改动把写入口收成**超管身份**(`LeshouyingMerchantController` 的
+--     save / delete 挂 `@perm.hasRole('SUPERUSER')`,2026-09-30 用户裁定
+--     「新增商户号是超管操作…租户不允许自己录入商户号」):租户角色即使持有本页也写不动,
+--     `page` 仍走权限串 `module_payment:leshouying:merchant:list`。
+--     (原文此处写的「新增时归属强制取自认证上下文,`save` 里 `entity.setTenantId(null)`」
+--     也已作废:那行已删,现在归属由超管在请求体里**显式指定且新增时必填**,
+--     缺即 400「请选择租户」——见 `LsyMerchantService.save`。)
+--   ⇒ 原文那个裁量口子(「你可以只删这一段」/「把 `CollectionMerchant` 从 `pages` 里删掉再执行」)
+--     **已关**:把本页授给租户不再附带任何写风险。
 --
 -- ── ★ 幂等 ────────────────────────────────────────────────────────────────
 --

+ 45 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyMerchantServiceTest.java

@@ -100,6 +100,32 @@ class LsyMerchantServiceTest {
         assertEquals(404, ex.getCode());
     }
 
+    /**
+     * ★ 文案不能把调用方送去一个**它自己进不去**的地方(2026-09-30)。
+     *
+     * <p>本次改动之前,「请先在「手工登记商户号」中登记」是**可照做**的指引 ——
+     * 那时租户能自助录入商户号。改动后录入改成**超管代录**
+     * ({@code LeshouyingMerchantController} 的 save/delete 挂 {@code @perm.hasRole('SUPERUSER')}):
+     * 租户既看不到新增按钮、端点也 403,同一句话就成了一条**死路** ——
+     * 操作员会照着它去那一页找一个不存在的按钮。
+     *
+     * <p>两侧都断言,缺一不可:只断言「不含旧说法」的话,把 message 换成空串也能绿。
+     * 前端同款文案见 {@code collection/merchant/index.vue} 的只读提示。
+     */
+    @Test
+    void requireByEnterprise_missing_message_routesToPlatformOps_notToTheNowReadOnlyPage() {
+        when(merchantMapper.selectOne(any())).thenReturn(null);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> service.requireByEnterprise(1L, "ENT1"));
+
+        assertEquals(404, ex.getCode());
+        String msg = ex.getMessage();
+        assertTrue(msg.contains("联系平台运营"), "租户能照做的动作只有「联系平台运营」。实测文案: " + msg);
+        assertFalse(msg.contains("手工登记商户号"),
+                "「手工登记商户号」对租户已只读 ⇒ 不能再叫它去那里登记。实测文案: " + msg);
+    }
+
     @Test
     void requireByEnterprise_blankEnterpriseId_throws() {
         assertThrows(BusinessException.class, () -> service.requireByEnterprise(1L, "  "));
@@ -135,6 +161,25 @@ class LsyMerchantServiceTest {
         assertTrue(ex.getMessage().contains("未配置商户号"), "实测文案: " + ex.getMessage());
     }
 
+    /**
+     * 同 {@code requireByEnterprise_missing_message_…}:租户级那条出口也要改口。
+     *
+     * <p>两个出口各钉一条、不合并成一条,是因为它们是**两处独立的字面量** ——
+     * 修一处不会让另一处变红。
+     */
+    @Test
+    void requireByTenant_missing_message_routesToPlatformOps_notToTheNowReadOnlyPage() {
+        when(merchantMapper.selectOne(any())).thenReturn(null);
+
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.requireByTenant(1L));
+
+        assertEquals(404, ex.getCode());
+        String msg = ex.getMessage();
+        assertTrue(msg.contains("联系平台运营"), "租户能照做的动作只有「联系平台运营」。实测文案: " + msg);
+        assertFalse(msg.contains("手工登记商户号"),
+                "「手工登记商户号」对租户已只读 ⇒ 不能再叫它去那里登记。实测文案: " + msg);
+    }
+
     /**
      * ★ 本组最要紧的一条:钉住**查询条件里带 {@code enterprise_id IS NULL}**。
      *