|
|
@@ -396,6 +396,87 @@ class HarShopServiceTest {
|
|
|
assertEquals("M700", req.getValue().getMerchantCode(), "发给上游的必须是调用方指定的那个");
|
|
|
}
|
|
|
|
|
|
+ // ==================== 超管建店(2026-10-09:门店改由超管创建) ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ 超管 + 显式 merchant_code ⇒ 按编号**全局**解析(无租户谓词),门店归属落
|
|
|
+ * **商户行的租户**(用户口径:「选的是哪个商户就用商户表的租户id」)。
|
|
|
+ *
|
|
|
+ * <p>两个断言各钉一件事:① {@code requireByCode} 被调用、租户腿没被调用;
|
|
|
+ * ② insert 那一刻的 {@code tenantId} == 商户行的租户。后者是这条链上最容易静默错的一环:
|
|
|
+ * AutoFillMetaObjectHandler **只在值为 null 时**才用 LoginUser 填 —— 少了显式 set,
|
|
|
+ * 超管(tenantId 可以是 null)建出来的门店要么无归属、要么落成超管自己的租户,
|
|
|
+ * 全程没有报错。dto 里保留 E1 是钉优先级:merchant_code 仍压过企业号。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_superuser_resolvesMerchantGlobally_andStampsMerchantTenant() {
|
|
|
+ HarMerchantEntity foreign = new HarMerchantEntity();
|
|
|
+ foreign.setTenantId(9L);
|
|
|
+ foreign.setEnterpriseId(null);
|
|
|
+ foreign.setMerchantCode("M9");
|
|
|
+ when(merchantService.requireByCode("M9")).thenReturn(foreign);
|
|
|
+ when(shopMapper.selectOne(any())).thenReturn(null);
|
|
|
+ snapshotStatusAtInsert();
|
|
|
+ when(harClient.createShop(any())).thenReturn(response("10000", "{\"shop_id\":\"S1\"}"));
|
|
|
+ HarShopCreateDTO dto = dto(); // 带 enterpriseId = "E1"
|
|
|
+ dto.setMerchantCode("M9");
|
|
|
+
|
|
|
+ HarShopVO vo = HarTestAuth.asSuperuser(() -> service.create(dto, null));
|
|
|
+
|
|
|
+ verify(merchantService).requireByCode("M9");
|
|
|
+ verify(merchantService, never()).requireByTenantAndCode(any(), any());
|
|
|
+ verify(merchantService, never()).requireByEnterpriseId(any());
|
|
|
+ ArgumentCaptor<HarShopEntity> captor = ArgumentCaptor.forClass(HarShopEntity.class);
|
|
|
+ verify(shopMapper).insert(captor.capture());
|
|
|
+ assertEquals(9L, captor.getValue().getTenantId(), "门店归属必须取自商户行,实测: "
|
|
|
+ + captor.getValue().getTenantId());
|
|
|
+ assertEquals("M9", vo.getMerchantCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ 超管**不指定商户**(也没有企业号)⇒ 400 明确拒绝,**不许**拿超管自己的租户回退。
|
|
|
+ *
|
|
|
+ * <p>钉的是「静默错归属」:超管带着 tenant_id 时(sys_user.tenant_id 可空也能有值),
|
|
|
+ * 回退到 requireByTenant(他自己的租户) 会挑出一条商户并把门店挂到那个租户下 ——
|
|
|
+ * 全程没有报错,与「选的是哪个商户就用商户表的租户id」相悖。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_superuserWithoutMerchantCode_rejectsInsteadOfGuessing() {
|
|
|
+ HarShopCreateDTO dto = dto();
|
|
|
+ dto.setEnterpriseId(null); // 企业、商户都不指定
|
|
|
+
|
|
|
+ BusinessException e = HarTestAuth.asSuperuser(7L, () ->
|
|
|
+ assertThrows(BusinessException.class, () -> service.create(dto, null)));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode(), "缺商户是入参问题,实测消息: " + e.getMessage());
|
|
|
+ assertTrue(e.getMessage().contains("merchant_code"),
|
|
|
+ "报错必须告诉调用方补哪个字段,实测消息: " + e.getMessage());
|
|
|
+ verify(merchantService, never()).requireByTenant(any());
|
|
|
+ verify(merchantService, never()).requireByCode(any());
|
|
|
+ verify(shopMapper, never()).insert(any());
|
|
|
+ verify(harClient, never()).createShop(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ 反向钉:**非超管**不得走全局解析 —— 租户 A 拿租户 B 的商户号建店仍是 404
|
|
|
+ * (这是 HarMerchantService#requireByTenantAndCode 租户谓词所守的安全边界;
|
|
|
+ * 平铺去掉租户校验会让持有建店权限的租户用户跨租户写,且上游真会建出门店)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_tenantUser_neverResolvesMerchantGlobally() {
|
|
|
+ when(merchantService.requireByTenantAndCode(7L, "M9"))
|
|
|
+ .thenThrow(new BusinessException(404, "商户编号不存在或不属于当前租户: M9"));
|
|
|
+ HarShopCreateDTO dto = dto();
|
|
|
+ dto.setMerchantCode("M9");
|
|
|
+
|
|
|
+ BusinessException e = HarTestAuth.asTenant(7L, () ->
|
|
|
+ assertThrows(BusinessException.class, () -> service.create(dto, null)));
|
|
|
+
|
|
|
+ assertEquals(404, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
+ verify(merchantService, never()).requireByCode(any());
|
|
|
+ verify(shopMapper, never()).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 租户级查重必须落成 {@code enterprise_id IS NULL} 而**不是** {@code = NULL}。
|
|
|
*
|