|
|
@@ -3,6 +3,7 @@ package com.payment.platform.module.payment.har.service;
|
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.payment.platform.common.exception.BusinessException;
|
|
|
import com.payment.platform.module.payment.har.client.HarClient;
|
|
|
import com.payment.platform.module.payment.har.entity.HarShopEntity;
|
|
|
import com.payment.platform.module.payment.har.enums.HarShopCreateStatus;
|
|
|
@@ -19,11 +20,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
+import java.util.Optional;
|
|
|
import java.util.Set;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
|
import static org.mockito.Mockito.verify;
|
|
|
@@ -42,12 +45,13 @@ class HarShopRegionPickTest {
|
|
|
@Mock private HarShopMapper shopMapper;
|
|
|
@Mock private HarMerchantService merchantService;
|
|
|
@Mock private HarClient harClient;
|
|
|
+ @Mock private HarIpRegionResolver ipRegionResolver;
|
|
|
|
|
|
private HarShopService service;
|
|
|
|
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
|
- service = new HarShopService(shopMapper, merchantService, harClient, new HarRegionCatalog());
|
|
|
+ service = new HarShopService(shopMapper, merchantService, harClient, new HarRegionCatalog(), ipRegionResolver);
|
|
|
// 同 HarShopServiceTest:让 LambdaQueryWrapper.getSqlSegment() 能在无 Spring 上下文时工作
|
|
|
MybatisConfiguration configuration = new MybatisConfiguration();
|
|
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), HarShopEntity.class);
|
|
|
@@ -78,21 +82,21 @@ class HarShopRegionPickTest {
|
|
|
@Test
|
|
|
void pick_cityTierWins() {
|
|
|
List<HarShopEntity> candidates = List.of(
|
|
|
- ok("NJ", "32", "3201"),
|
|
|
- ok("SZ", "32", "3205"));
|
|
|
+ ok("NJ", "320000", "320100"),
|
|
|
+ ok("SZ", "320000", "320500"));
|
|
|
|
|
|
- assertEquals("NJ", pick(candidates, "32", "3201", "114.114.114.114").getThirdShopId());
|
|
|
+ assertEquals("NJ", pick(candidates, "320000", "320100", "114.114.114.114").getThirdShopId());
|
|
|
}
|
|
|
|
|
|
/** 同城没店 ⇒ 退到同省,且**只**在省内挑(异省的店绝不越界) */
|
|
|
@Test
|
|
|
void pick_cityMissing_fallsBackToTheSameProvinceOnly() {
|
|
|
List<HarShopEntity> candidates = List.of(
|
|
|
- ok("SZ", "32", "3205"),
|
|
|
- ok("WX", "32", "3202"),
|
|
|
- ok("HZ", "33", "3301"));
|
|
|
+ ok("SZ", "320000", "320500"),
|
|
|
+ ok("WX", "320000", "320200"),
|
|
|
+ ok("HZ", "330000", "330100"));
|
|
|
|
|
|
- HarShopEntity picked = pick(candidates, "32", "3201", "114.114.114.114");
|
|
|
+ HarShopEntity picked = pick(candidates, "320000", "320100", "114.114.114.114");
|
|
|
|
|
|
assertNotNull(picked);
|
|
|
assertTrue(Set.of("SZ", "WX").contains(picked.getThirdShopId()),
|
|
|
@@ -105,27 +109,27 @@ class HarShopRegionPickTest {
|
|
|
*/
|
|
|
@Test
|
|
|
void pick_cityCodeUnderAnotherProvinceIsNotTrusted() {
|
|
|
- List<HarShopEntity> candidates = List.of(ok("X", "99", "3201"));
|
|
|
+ List<HarShopEntity> candidates = List.of(ok("X", "999999", "320100"));
|
|
|
|
|
|
- assertNull(pick(candidates, "32", "3201", "114.114.114.114"));
|
|
|
+ assertNull(pick(candidates, "320000", "320100", "114.114.114.114"));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
void pick_blankProvinceCode_picksNothing() {
|
|
|
- List<HarShopEntity> candidates = List.of(ok("NJ", "32", "3201"));
|
|
|
+ List<HarShopEntity> candidates = List.of(ok("NJ", "320000", "320100"));
|
|
|
|
|
|
- assertNull(pick(candidates, null, "3201", "114.114.114.114"));
|
|
|
- assertNull(pick(candidates, " ", "3201", "114.114.114.114"));
|
|
|
+ assertNull(pick(candidates, null, "320100", "114.114.114.114"));
|
|
|
+ assertNull(pick(candidates, " ", "320100", "114.114.114.114"));
|
|
|
// 正控:同一个候选集,省码给对就必须挑得出来(否则上两条是量具坏了在假绿)
|
|
|
- assertNotNull(pick(candidates, "32", "3201", "114.114.114.114"));
|
|
|
+ assertNotNull(pick(candidates, "320000", "320100", "114.114.114.114"));
|
|
|
}
|
|
|
|
|
|
/** 空 city 只走省级(ipwho.is 偶尔只给到省) */
|
|
|
@Test
|
|
|
void pick_blankCityCode_stillUsesTheProvinceTier() {
|
|
|
- List<HarShopEntity> candidates = List.of(ok("NJ", "32", "3201"));
|
|
|
+ List<HarShopEntity> candidates = List.of(ok("NJ", "320000", "320100"));
|
|
|
|
|
|
- assertEquals("NJ", pick(candidates, "32", null, "114.114.114.114").getThirdShopId());
|
|
|
+ assertEquals("NJ", pick(candidates, "320000", null, "114.114.114.114").getThirdShopId());
|
|
|
}
|
|
|
|
|
|
// ──────────────────────────── 什么算「能用的店」 ────────────────────────────
|
|
|
@@ -138,31 +142,31 @@ class HarShopRegionPickTest {
|
|
|
@Test
|
|
|
void pick_onlyConsidersSuccessfullyCreatedShops() {
|
|
|
List<HarShopEntity> unusable = List.of(
|
|
|
- shop("FAILED", "32", "3201", HarShopCreateStatus.FAILED, "S1"),
|
|
|
- shop("PENDING", "32", "3201", HarShopCreateStatus.PENDING, "S2"),
|
|
|
- shop("NOID", "32", "3201", HarShopCreateStatus.SUCCESS, null),
|
|
|
- shop("BLANKID", "32", "3201", HarShopCreateStatus.SUCCESS, " "),
|
|
|
- shop("NOSTATUS", "32", "3201", null, "S5"));
|
|
|
- assertNull(pick(unusable, "32", "3201", "114.114.114.114"), "未建成的店不能拿去下单");
|
|
|
+ shop("FAILED", "320000", "320100", HarShopCreateStatus.FAILED, "S1"),
|
|
|
+ shop("PENDING", "320000", "320100", HarShopCreateStatus.PENDING, "S2"),
|
|
|
+ shop("NOID", "320000", "320100", HarShopCreateStatus.SUCCESS, null),
|
|
|
+ shop("BLANKID", "320000", "320100", HarShopCreateStatus.SUCCESS, " "),
|
|
|
+ shop("NOSTATUS", "320000", "320100", null, "S5"));
|
|
|
+ assertNull(pick(unusable, "320000", "320100", "114.114.114.114"), "未建成的店不能拿去下单");
|
|
|
|
|
|
List<HarShopEntity> withOneGood = new ArrayList<>(unusable);
|
|
|
- withOneGood.add(ok("GOOD", "32", "3201"));
|
|
|
- assertEquals("GOOD", pick(withOneGood, "32", "3201", "114.114.114.114").getThirdShopId());
|
|
|
+ withOneGood.add(ok("GOOD", "320000", "320100"));
|
|
|
+ assertEquals("GOOD", pick(withOneGood, "320000", "320100", "114.114.114.114").getThirdShopId());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
void pick_noCandidatesAtAll_picksNothing() {
|
|
|
- assertNull(pick(List.of(), "32", "3201", "114.114.114.114"));
|
|
|
+ assertNull(pick(List.of(), "320000", "320100", "114.114.114.114"));
|
|
|
}
|
|
|
|
|
|
// ──────────────────────────── 同地区多店:按 IP 稳定散列 ────────────────────────────
|
|
|
|
|
|
private static List<HarShopEntity> fourShopsInJiangsu() {
|
|
|
return List.of(
|
|
|
- ok("A", "32", "3201"),
|
|
|
- ok("B", "32", "3201"),
|
|
|
- ok("C", "32", "3201"),
|
|
|
- ok("D", "32", "3201"));
|
|
|
+ ok("A", "320000", "320100"),
|
|
|
+ ok("B", "320000", "320100"),
|
|
|
+ ok("C", "320000", "320100"),
|
|
|
+ ok("D", "320000", "320100"));
|
|
|
}
|
|
|
|
|
|
/** 同一个顾客(同 IP)在缓存过期后重来一次,必须还是那一家 */
|
|
|
@@ -172,8 +176,8 @@ class HarShopRegionPickTest {
|
|
|
|
|
|
for (int i = 0; i < 50; i++) {
|
|
|
String seed = "10.0." + (i / 250) + "." + (i % 250);
|
|
|
- assertEquals(pick(candidates, "32", "3201", seed).getThirdShopId(),
|
|
|
- pick(candidates, "32", "3201", seed).getThirdShopId(),
|
|
|
+ assertEquals(pick(candidates, "320000", "320100", seed).getThirdShopId(),
|
|
|
+ pick(candidates, "320000", "320100", seed).getThirdShopId(),
|
|
|
"同一 IP 必须稳定落到同一家,seed=" + seed);
|
|
|
}
|
|
|
}
|
|
|
@@ -185,7 +189,7 @@ class HarShopRegionPickTest {
|
|
|
Set<String> chosen = new HashSet<>();
|
|
|
|
|
|
for (int i = 0; i < 400; i++) {
|
|
|
- chosen.add(pick(candidates, "32", "3201", "10.0." + (i / 250) + "." + (i % 250)).getThirdShopId());
|
|
|
+ chosen.add(pick(candidates, "320000", "320100", "10.0." + (i / 250) + "." + (i % 250)).getThirdShopId());
|
|
|
}
|
|
|
|
|
|
assertEquals(Set.of("A", "B", "C", "D"), chosen, "400 个不同 IP 应把所有店都用上");
|
|
|
@@ -196,10 +200,10 @@ class HarShopRegionPickTest {
|
|
|
void pick_blankSeedIsStillDeterministic() {
|
|
|
List<HarShopEntity> candidates = fourShopsInJiangsu();
|
|
|
|
|
|
- HarShopEntity first = pick(candidates, "32", "3201", null);
|
|
|
+ HarShopEntity first = pick(candidates, "320000", "320100", null);
|
|
|
assertNotNull(first);
|
|
|
- assertEquals(first.getThirdShopId(), pick(candidates, "32", "3201", null).getThirdShopId());
|
|
|
- assertEquals(first.getThirdShopId(), pick(candidates, "32", "3201", " ").getThirdShopId());
|
|
|
+ assertEquals(first.getThirdShopId(), pick(candidates, "320000", "320100", null).getThirdShopId());
|
|
|
+ assertEquals(first.getThirdShopId(), pick(candidates, "320000", "320100", " ").getThirdShopId());
|
|
|
}
|
|
|
|
|
|
// ──────────────────────────── 查询本身 ────────────────────────────
|
|
|
@@ -215,7 +219,7 @@ class HarShopRegionPickTest {
|
|
|
ArgumentCaptor<LambdaQueryWrapper<HarShopEntity>> captor =
|
|
|
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
|
|
|
- assertNull(service.pickForRegion("M001", "32", "3201", "114.114.114.114"));
|
|
|
+ assertNull(service.pickForRegion("M001", "320000", "320100", "114.114.114.114"));
|
|
|
|
|
|
verify(shopMapper).selectList(captor.capture());
|
|
|
String sql = captor.getValue().getSqlSegment();
|
|
|
@@ -225,7 +229,7 @@ class HarShopRegionPickTest {
|
|
|
/** 商户号本身为空就别去查库了(空串会变成 merchant_code = '',查出一片空并静默拒收) */
|
|
|
@Test
|
|
|
void pickForRegion_blankMerchantCode_picksNothingWithoutQuerying() {
|
|
|
- assertNull(service.pickForRegion(" ", "32", "3201", "114.114.114.114"));
|
|
|
+ assertNull(service.pickForRegion(" ", "320000", "320100", "114.114.114.114"));
|
|
|
|
|
|
verify(shopMapper, org.mockito.Mockito.never()).selectList(any());
|
|
|
}
|
|
|
@@ -234,9 +238,199 @@ class HarShopRegionPickTest {
|
|
|
@Test
|
|
|
void pickForRegion_picksFromWhatTheQueryReturned() {
|
|
|
when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
- ok("NJ", "32", "3201"),
|
|
|
- ok("HZ", "33", "3301")));
|
|
|
+ ok("NJ", "320000", "320100"),
|
|
|
+ ok("HZ", "330000", "330100")));
|
|
|
|
|
|
- assertEquals("NJ", service.pickForRegion("M001", "32", "3201", "114.114.114.114").getThirdShopId());
|
|
|
+ assertEquals("NJ", service.pickForRegion("M001", "320000", "320100", "114.114.114.114").getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ──────────────── 下单按顾客归属挑店(开放 API,2026-10-09 改版:IP 优先、省码回退) ────────────────
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与收款链接腿的差别只在**地区来源**:那边永远是顾客请求 IP 解析出的地名,这边是
|
|
|
+ * 调用方给的**顾客 IP 或 GB/T 2260 省码**。阶梯本身上面各条已钉住,这里钉
|
|
|
+ * 「来源怎么定优先级、挑不出来怎么办」。
|
|
|
+ */
|
|
|
+ private static HarIpRegionResolver.IpGeo ipGeoHunan(String cityName) {
|
|
|
+ // 与真实回包同形:省带「省」、市不带「市」
|
|
|
+ return new HarIpRegionResolver.IpGeo("CN", "湖南省", "HN", cityName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** IP 能解析出归属地 ⇒ 以它为准,哪怕同时传的 province_code 指向别处(IP 更精确) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipResolves_beatsTheProvinceCode() {
|
|
|
+ when(ipRegionResolver.lookup("114.114.114.114")).thenReturn(Optional.of(ipGeoHunan("长沙")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ ok("NJ", "320000", "320100"),
|
|
|
+ ok("CS", "430000", "430100")));
|
|
|
+
|
|
|
+ assertEquals("CS", service.pickForOrder("M001", "320000", "114.114.114.114", "BIZ-1").getThirdShopId(),
|
|
|
+ "省码是江苏、IP 在湖南 ⇒ 挑湖南的店");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** IP 解析到了省、市名对不上目录(上游常回区级名)⇒ 退到 IP 所在省挑,不报错也不改用省码 */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipCityUnknown_fallsBackToTheIpProvince() {
|
|
|
+ when(ipRegionResolver.lookup("114.114.114.114")).thenReturn(Optional.of(ipGeoHunan("渝中區")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ ok("CS", "430000", "430100"),
|
|
|
+ ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ assertEquals("CS", service.pickForOrder("M001", "320000", "114.114.114.114", "BIZ-1").getThirdShopId(),
|
|
|
+ "IP 的省里有店就不该回退到调用方传的省码");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** IP 归属地一家可用门店都没有 ⇒ 回退 province_code 继续挑(2026-10-09 用户裁定) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipRegionHasNoShop_fallsBackToProvinceCode() {
|
|
|
+ when(ipRegionResolver.lookup("114.114.114.114")).thenReturn(Optional.of(ipGeoHunan("长沙")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ assertEquals("NJ", service.pickForOrder("M001", "320000", "114.114.114.114", "BIZ-1").getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 挑中的店没有 third_shop_id(脏数据)与「没挑到」同权 ⇒ 同样回退省码 */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipPickYieldsShopWithoutThirdShopId_fallsBackToProvinceCode() {
|
|
|
+ when(ipRegionResolver.lookup("114.114.114.114")).thenReturn(Optional.of(ipGeoHunan("长沙")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ shop(null, "430000", "430100", HarShopCreateStatus.SUCCESS, "S-PLATFORM"),
|
|
|
+ ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ assertEquals("NJ", service.pickForOrder("M001", "320000", "114.114.114.114", "BIZ-1").getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** IP 解析不出(内网/非大陆/取数失败)⇒ 有省码就按省挑 —— 这正是「两个可以同时传」的用法 */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipUnresolvable_fallsBackToProvinceCode() {
|
|
|
+ when(ipRegionResolver.lookup("1.2.3.4")).thenReturn(Optional.empty());
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ assertEquals("NJ", service.pickForOrder("M001", "320000", "1.2.3.4", "BIZ-1").getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 解析出了但省名不在目录里 ⇒ 同样回退省码(港澳台已被解析器在更上游挡掉) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipRegionNameNotInCatalog_fallsBackToProvinceCode() {
|
|
|
+ when(ipRegionResolver.lookup("1.2.3.4"))
|
|
|
+ .thenReturn(Optional.of(new HarIpRegionResolver.IpGeo("CN", "某某省", "XX", "某市")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ assertEquals("NJ", service.pickForOrder("M001", "320000", "1.2.3.4", "BIZ-1").getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 只传 ip 且解析不出:是调用方的输入错误,响亮拒绝 —— 静默不带门店下单 = 地区意图悄悄落空 */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipOnly_unresolvable_400() {
|
|
|
+ when(ipRegionResolver.lookup("1.2.3.4")).thenReturn(Optional.empty());
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", null, "1.2.3.4", "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode());
|
|
|
+ assertEquals("用户 IP 无法解析出归属地: 1.2.3.4", e.getMessage());
|
|
|
+ verify(shopMapper, org.mockito.Mockito.never()).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 只传 ip、解析出了地区但一家店都没有:报的是**那个地区**,不是「解析不出」 */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_ipOnly_regionHasNoShop_400_namesTheIpRegion() {
|
|
|
+ when(ipRegionResolver.lookup("114.114.114.114")).thenReturn(Optional.of(ipGeoHunan("长沙")));
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(ok("NJ", "320000", "320100")));
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", null, "114.114.114.114", "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals("该地区没有可用门店: 430100(省 430000)", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 两个都没传(上线路径由 create 的守卫挡住,这里防将来直调时抛 NPE) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_neitherIpNorProvince_400() {
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", null, null, "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 只传省份、省内多店、没传 out_biz_no ⇒ 随机种子,仍要散开(不能退化成固定第一家) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_provinceOnly_blankSeed_spreadsAcrossCalls() {
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ ok("A", "320000", "320100"),
|
|
|
+ ok("B", "320000", "320100"),
|
|
|
+ ok("C", "320000", "320500"),
|
|
|
+ ok("D", "320000", "320500")));
|
|
|
+
|
|
|
+ Set<String> chosen = new HashSet<>();
|
|
|
+ for (int i = 0; i < 60; i++) {
|
|
|
+ chosen.add(service.pickForOrder("M001", "320000", null, null).getThirdShopId());
|
|
|
+ }
|
|
|
+
|
|
|
+ assertTrue(chosen.size() > 1, "没传 out_biz_no 时也必须是随机挑而不是固定第一家,实际: " + chosen);
|
|
|
+ assertTrue(Set.of("A", "B", "C", "D").containsAll(chosen));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 稳定散列的承诺不变:给了 out_biz_no,同一单重复挑落同一家(开放文档 10.2 写死了这条) */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_sameOutBizNo_isStable() {
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ ok("A", "320000", "320100"),
|
|
|
+ ok("B", "320000", "320100"),
|
|
|
+ ok("C", "320000", "320500"),
|
|
|
+ ok("D", "320000", "320500")));
|
|
|
+
|
|
|
+ String first = service.pickForOrder("M001", "320000", null, "BIZ-1").getThirdShopId();
|
|
|
+ for (int i = 0; i < 10; i++) {
|
|
|
+ assertEquals(first, service.pickForOrder("M001", "320000", null, "BIZ-1").getThirdShopId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pickForOrder_provinceOnly_stillPicksWithinProvince() {
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ ok("SZ", "320000", "320500"),
|
|
|
+ ok("HZ", "330000", "330100")));
|
|
|
+
|
|
|
+ HarShopEntity picked = service.pickForOrder("M001", "320000", null, "BIZ-1");
|
|
|
+
|
|
|
+ assertEquals("SZ", picked.getThirdShopId(), "不给 IP ⇒ 按全省挑,异省的店绝不越界");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pickForOrder_unknownProvinceCode_400_sameMessageAsShopCreate() {
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", "999999", null, "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode());
|
|
|
+ assertEquals("省份编码不存在: 999999", e.getMessage(),
|
|
|
+ "与门店创建那条必须一字不差 —— 开放文档 10.1/10.2 引用的是同一句话");
|
|
|
+ verify(shopMapper, org.mockito.Mockito.never()).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pickForOrder_noUsableShopInProvince_400_namesTheProvince() {
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(ok("HZ", "330000", "330100")));
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", "320000", null, "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals("该地区没有可用门店: 省 320000", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 挑中了但 third_shop_id 为空 ⇒ 与「没挑到」同一条 400。结账腿有同样的守卫
|
|
|
+ * ({@code pickShopForCustomer} 那两行);不守的话会拿一个空编码去下单,
|
|
|
+ * 调用方以为挑店生效了,实际风控归属是空的。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void pickForOrder_pickedShopWithoutThirdShopId_400() {
|
|
|
+ when(shopMapper.selectList(any())).thenReturn(List.of(
|
|
|
+ shop(null, "320000", "320100", HarShopCreateStatus.SUCCESS, "S-PLATFORM")));
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.pickForOrder("M001", "320000", null, "BIZ-1"));
|
|
|
+
|
|
|
+ assertEquals("该地区没有可用门店: 省 320000", e.getMessage());
|
|
|
}
|
|
|
}
|