|
|
@@ -1,6 +1,7 @@
|
|
|
package com.payment.platform.module.payment.leshouying.service;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
@@ -104,6 +105,71 @@ class LsyMerchantServiceTest {
|
|
|
assertEquals(404, ex.getCode());
|
|
|
}
|
|
|
|
|
|
+ // ─────────────────────── requireByTenant(租户级商户,2026-09-22 去企业化) ───────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void requireByTenant_found_returnsEntity() {
|
|
|
+ LsyMerchantEntity m = new LsyMerchantEntity();
|
|
|
+ m.setMchNo("Y52211090004");
|
|
|
+ when(merchantMapper.selectOne(any())).thenReturn(m);
|
|
|
+
|
|
|
+ assertEquals("Y52211090004", service.requireByTenant(1L).getMchNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void requireByTenant_missing_throws404() {
|
|
|
+ when(merchantMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class, () -> service.requireByTenant(1L));
|
|
|
+
|
|
|
+ assertEquals(404, ex.getCode());
|
|
|
+ assertTrue(ex.getMessage().contains("未配置商户号"), "实测文案: " + ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ 本组最要紧的一条:钉住**查询条件里带 {@code enterprise_id IS NULL}**。
|
|
|
+ *
|
|
|
+ * <p>上面两条用例只喂 mock、不看 SQL,漏掉 {@code isNull} 时它们**照样全绿** ——
|
|
|
+ * 而查询已经退化成「该租户的任意一行商户映射」,捞到**企业级**那一行也照常返回。
|
|
|
+ * 企业级与租户级的语义完全不同(前者绑企业、后者不绑),静默串用是本轮改造最想防的事。
|
|
|
+ *
|
|
|
+ * <p>条件串按 {@code AND} 切开做**集合断言**,不用 contains —— 同文件里
|
|
|
+ * {@code review_status} 含 {@code status} 子串的坑是同一类。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void requireByTenant_queriesNullEnterpriseId() {
|
|
|
+ when(merchantMapper.selectOne(any())).thenReturn(row(EDIT_ID));
|
|
|
+
|
|
|
+ service.requireByTenant(7L);
|
|
|
+
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(merchantMapper).selectOne(captor.capture());
|
|
|
+ LambdaQueryWrapper<?> w = captor.getValue();
|
|
|
+ String seg = w.getSqlSegment();
|
|
|
+
|
|
|
+ // ★ 实测(不要照模型写):getSqlSegment() **包含** .last() 追加的那一段,
|
|
|
+ // 形状是 `(tenant_id = #{…MPGENVAL1} AND enterprise_id IS NULL) LIMIT 1`。
|
|
|
+ // 条件里没有嵌套括号,所以第一个 ')' 就是条件组的收尾。
|
|
|
+ assertTrue(seg.startsWith("("), "WHERE 应以条件组开头。实测: [" + seg + "]");
|
|
|
+ int close = seg.indexOf(')');
|
|
|
+ assertTrue(close > 0, "WHERE 条件组应有收尾括号。实测: [" + seg + "]");
|
|
|
+ assertEquals("LIMIT 1", seg.substring(close + 1).trim(),
|
|
|
+ "selectOne 必须带 LIMIT 1(租户级映射理论上唯一,但别依赖它)。实测: [" + seg + "]");
|
|
|
+
|
|
|
+ Set<String> conds = Arrays.stream(seg.substring(1, close).split(" AND "))
|
|
|
+ .map(String::trim).collect(Collectors.toSet());
|
|
|
+
|
|
|
+ assertTrue(conds.contains("enterprise_id IS NULL"),
|
|
|
+ "必须带 enterprise_id IS NULL —— 漏了它就会捞到企业级商户。实测条件: " + conds);
|
|
|
+ assertEquals(1, conds.stream().filter(c -> c.startsWith("tenant_id = ")).count(),
|
|
|
+ "必须恰有一条租户条件(少了会跨租户串号)。实测条件: " + conds);
|
|
|
+ assertEquals(2, conds.size(), "条件应恰好两条。实测条件: " + conds);
|
|
|
+
|
|
|
+ assertEquals(7L, w.getParamNameValuePairs().values().stream()
|
|
|
+ .filter(Long.class::isInstance).findFirst().orElse(null),
|
|
|
+ "租户条件的占位符应解析成入参 tenantId。实测: " + w.getParamNameValuePairs());
|
|
|
+ }
|
|
|
+
|
|
|
// ─────────────────────── save():编辑分支(D-1 / D-2 / 0 行判据) ───────────────────────
|
|
|
|
|
|
@Test
|
|
|
@@ -241,6 +307,34 @@ class LsyMerchantServiceTest {
|
|
|
"实测参数表: " + w.getParamNameValuePairs());
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 去企业化:企业字段留空 ⇒ 写 NULL(变成租户级映射)。
|
|
|
+ *
|
|
|
+ * <p>改前这里是 **400**(那时 {@code pay_lsy_merchant.enterprise_id} 是 {@code NOT NULL},
|
|
|
+ * 留空没有合法落点)。列放开后 400 的前提消失,留空有了明确语义。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void save_updateBranch_blankEnterpriseId_writesNull() {
|
|
|
+ when(merchantMapper.update(any(), any())).thenReturn(1);
|
|
|
+ when(merchantMapper.selectById(any())).thenReturn(row(EDIT_ID));
|
|
|
+
|
|
|
+ LsyMerchantEntity body = new LsyMerchantEntity();
|
|
|
+ body.setId(EDIT_ID);
|
|
|
+ body.setEnterpriseId(""); // 操作者把一条企业级映射改成租户级
|
|
|
+ body.setMchNo("Y52211090004");
|
|
|
+
|
|
|
+ service.save(body); // 不得抛 400
|
|
|
+
|
|
|
+ LambdaUpdateWrapper<?> w = capturedUpdate();
|
|
|
+ assertTrue(setColumns(w).contains("enterprise_id"),
|
|
|
+ "enterprise_id 仍必须在 SET 里(当成 skip 就会保留旧企业)。实测: " + w.getSqlSet());
|
|
|
+ // 与三个可空列一起,共 4 个 NULL —— 空串会以 "" 入参数表,
|
|
|
+ // 而 requireByTenant 找的是 IS NULL,那一行将永远查不到
|
|
|
+ assertEquals(4, w.getParamNameValuePairs().values().stream().filter(v -> v == null).count(),
|
|
|
+ "enterprise_id 应与三个可空列一样以 NULL 入 SET。实测参数表: " + w.getParamNameValuePairs());
|
|
|
+ assertNull(body.getEnterpriseId(), "空 ⇒ NULL 的收敛点在 service 层");
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void save_updateBranch_zeroAffectedRows_throws404() {
|
|
|
// 「id 打不中目标行」:精度丢失后对不上、或这行已被另一个页签删掉
|
|
|
@@ -289,6 +383,27 @@ class LsyMerchantServiceTest {
|
|
|
verify(merchantMapper, never()).updateById(any());
|
|
|
}
|
|
|
|
|
|
+ /** 去企业化:不填企业也能新增一条映射(租户级)。改前这里是 400。 */
|
|
|
+ @Test
|
|
|
+ void save_insertBranch_blankEnterpriseId_inserts() {
|
|
|
+ LsyMerchantEntity body = new LsyMerchantEntity();
|
|
|
+ body.setEnterpriseId(" "); // 只有空白 —— 必须与 null 同等对待
|
|
|
+ body.setMchNo("Y52211090004");
|
|
|
+ when(merchantMapper.insert(any())).thenAnswer(inv -> {
|
|
|
+ ((LsyMerchantEntity) inv.getArgument(0)).setId(9L);
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+ when(merchantMapper.selectById(9L)).thenReturn(row(9L));
|
|
|
+
|
|
|
+ service.save(body); // 不得抛 400
|
|
|
+
|
|
|
+ ArgumentCaptor<LsyMerchantEntity> inserted = ArgumentCaptor.forClass(LsyMerchantEntity.class);
|
|
|
+ verify(merchantMapper).insert(inserted.capture());
|
|
|
+ assertNull(inserted.getValue().getEnterpriseId(),
|
|
|
+ "租户级商户的 enterprise_id 必须是 NULL —— 存成 \" \" 会让 requireByTenant 永远查不到它");
|
|
|
+ verify(merchantMapper, never()).update(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
// ─────────────────────────────────── 夹具 ───────────────────────────────────
|
|
|
|
|
|
/** 页面送来的编辑请求体:只有 `id` + 5 个业务键(`merchant/index.vue` 的 FORM_FIELDS),服务端字段一个都不带 */
|