4 changed files with 270 additions and 2 deletions
@ -0,0 +1,143 @@ |
|||
package com.crm.rule.config; |
|||
|
|||
import com.crm.rule.domain.entity.SysRegion; |
|||
import com.crm.rule.service.ISysRegionService; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.boot.CommandLineRunner; |
|||
import org.springframework.core.annotation.Order; |
|||
import org.springframework.core.io.ClassPathResource; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 行政区划全量幂等种子导入(GB/T 2260 省/市/区三级,约 3400 行) |
|||
* <p>数据源:{@code resources/data/pca-code.json}(china-division 2.7.0,国家统计局口径, |
|||
* 2 位省码/4 位市码/6 位区县码),种子化时统一补零为 6 位国标码("11" → "110000"), |
|||
* 保持 code 前缀嵌套({@code LIKE '4401%'} 祖先查询可用)。港澳台仅省级 |
|||
* (810000/820000/710000),满足公海池「省份多选兼容港澳」场景。</p> |
|||
* <p>东莞/中山等直辖县级市的镇街区划为 9 位码(如 441900003),保留原样并右补零到 |
|||
* 12 位(与实体注释「6 位或 12 位」对齐),前缀嵌套仍成立。</p> |
|||
* <p>幂等:每层整体走 {@code batchUpsert}(INSERT ... ON DUPLICATE KEY UPDATE,命中 |
|||
* uk_region_code 唯一索引),存在则刷新名称、不存在则插入,永不删除。 |
|||
* 父子挂接分三层「upsert → 回查拿 id → 下一层」,因为 upsert 不回填主键。</p> |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@Order(12) |
|||
@RequiredArgsConstructor |
|||
public class RegionDataInitializer implements CommandLineRunner { |
|||
|
|||
private static final String DATA_PATH = "data/pca-code.json"; |
|||
|
|||
private final ISysRegionService sysRegionService; |
|||
|
|||
@Override |
|||
public void run(String... args) throws Exception { |
|||
log.info("执行行政区划全量种子导入检查..."); |
|||
JsonNode root = new ObjectMapper().readTree( |
|||
new ClassPathResource(DATA_PATH).getInputStream()); |
|||
|
|||
Map<String, SysRegion> existing = sysRegionService.list().stream() |
|||
.collect(Collectors.toMap(SysRegion::getCode, Function.identity(), (a, b) -> a)); |
|||
int existingCount = existing.size(); |
|||
int total = 0; |
|||
|
|||
// ---- 第一层:省级(parentId=null)----
|
|||
List<SysRegion> provinces = new ArrayList<>(); |
|||
for (JsonNode node : root) { |
|||
provinces.add(build(node, 1, null, existing)); |
|||
} |
|||
total += provinces.size(); |
|||
if (!provinces.isEmpty()) { |
|||
sysRegionService.batchUpsert(provinces); |
|||
} |
|||
Map<String, Long> provinceIds = reloadIds(provinces); |
|||
|
|||
// ---- 第二层:市级(父 = 省级实体)----
|
|||
List<SysRegion> cities = new ArrayList<>(); |
|||
for (JsonNode province : root) { |
|||
Long provinceId = provinceIds.get(padded(province)); |
|||
for (JsonNode city : province.path("children")) { |
|||
cities.add(build(city, 2, provinceId, existing)); |
|||
} |
|||
} |
|||
total += cities.size(); |
|||
if (!cities.isEmpty()) { |
|||
sysRegionService.batchUpsert(cities); |
|||
} |
|||
Map<String, Long> cityIds = reloadIds(cities); |
|||
|
|||
// ---- 第三层:区县级(父 = 市级实体)----
|
|||
List<SysRegion> areas = new ArrayList<>(); |
|||
for (JsonNode province : root) { |
|||
for (JsonNode city : province.path("children")) { |
|||
Long cityId = cityIds.get(padded(city)); |
|||
for (JsonNode area : city.path("children")) { |
|||
areas.add(build(area, 3, cityId, existing)); |
|||
} |
|||
} |
|||
} |
|||
total += areas.size(); |
|||
if (!areas.isEmpty()) { |
|||
sysRegionService.batchUpsert(areas); |
|||
} |
|||
|
|||
log.info("行政区划种子导入完成:对账 {} 行(省 {} / 市 {} / 区 {}),存量 {} 行,新增 {} 行", |
|||
total, provinces.size(), cities.size(), areas.size(), |
|||
existingCount, total - existingCount); |
|||
} |
|||
|
|||
/*-------- 构建实体:已存在保留原 id,新建 id=null 由 upsert 插入(包私有供测试)--------*/ |
|||
|
|||
SysRegion build(JsonNode node, int level, Long parentId, Map<String, SysRegion> existing) { |
|||
String code = padded(node); |
|||
SysRegion hit = existing.get(code); |
|||
if (hit != null) { |
|||
hit.setName(node.path("name").asText()); |
|||
hit.setLevel(level); |
|||
hit.setParentId(parentId); |
|||
return hit; |
|||
} |
|||
SysRegion entity = new SysRegion(); |
|||
entity.setCode(code); |
|||
entity.setName(node.path("name").asText()); |
|||
entity.setLevel(level); |
|||
entity.setParentId(parentId); |
|||
return entity; |
|||
} |
|||
|
|||
/*-------- upsert 不回填主键,回查拿 code → id --------*/ |
|||
|
|||
private Map<String, Long> reloadIds(List<SysRegion> entities) { |
|||
Set<String> codes = entities.stream().map(SysRegion::getCode).collect(Collectors.toSet()); |
|||
if (codes.isEmpty()) { |
|||
return Map.of(); |
|||
} |
|||
Map<String, Long> ids = new HashMap<>(); |
|||
sysRegionService.lambdaQuery() |
|||
.select(SysRegion::getId, SysRegion::getCode) |
|||
.in(SysRegion::getCode, codes) |
|||
.list() |
|||
.forEach(e -> ids.put(e.getCode(), e.getId())); |
|||
return ids; |
|||
} |
|||
|
|||
/*-------- 编码规范化(包私有供测试):省/市/区补零到 6 位; |
|||
东莞/中山等直辖县级市的镇街 9 位码补零到 12 位 --------*/ |
|||
|
|||
String padded(JsonNode node) { |
|||
String code = node.path("code").asText(); |
|||
int target = code.length() > 6 ? 12 : 6; |
|||
return code.length() >= target ? code : code + "0".repeat(target - code.length()); |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,121 @@ |
|||
package com.crm.rule.config; |
|||
|
|||
import com.crm.rule.domain.entity.SysRegion; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import org.junit.jupiter.api.BeforeAll; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.core.io.ClassPathResource; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.HashSet; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
/** |
|||
* 行政区划种子数据与纯逻辑校验(不落库,DB 行为由联调环境启动验证): |
|||
* <ul> |
|||
* <li>数据文件完整性:34 省级(含港澳台)/ 342 市 / 3056 区县,编码补零后全 6 位且不重复</li> |
|||
* <li>补零规则:"11" → "110000"、"1101" → "110100"、6 位原样</li> |
|||
* <li>实体构建:新建无 id、已存在复用并刷新名称/层级/父级</li> |
|||
* </ul> |
|||
*/ |
|||
@DisplayName("行政区划种子数据完整性与补零/构建逻辑") |
|||
class RegionDataInitializerTest { |
|||
|
|||
private static JsonNode root; |
|||
private static RegionDataInitializer initializer; |
|||
|
|||
@BeforeAll |
|||
static void loadData() throws Exception { |
|||
root = new ObjectMapper().readTree( |
|||
new ClassPathResource("data/pca-code.json").getInputStream()); |
|||
initializer = new RegionDataInitializer(null); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("数据文件:34 省(含港澳台)/ 342 市 / 3056 区县,补零后 6 位码全局唯一") |
|||
void dataFile_fullCoverageAndUniqueCodes() { |
|||
assertThat(root.isArray()).isTrue(); |
|||
assertThat(root.size()).as("省级数量(31 大陆 + 港澳台 3)").isEqualTo(34); |
|||
|
|||
int cities = 0; |
|||
int areas = 0; |
|||
Set<String> codes = new HashSet<>(); |
|||
for (JsonNode province : root) { |
|||
String pCode = initializer.padded(province); |
|||
assertThat(pCode).hasSize(6); |
|||
assertThat(codes.add(pCode)).as("省级码唯一: " + pCode).isTrue(); |
|||
for (JsonNode city : province.path("children")) { |
|||
cities++; |
|||
String cCode = initializer.padded(city); |
|||
assertThat(cCode).hasSize(6).startsWith(pCode.substring(0, 2)); |
|||
assertThat(codes.add(cCode)).as("市级码唯一: " + cCode).isTrue(); |
|||
for (JsonNode area : city.path("children")) { |
|||
areas++; |
|||
String aCode = initializer.padded(area); |
|||
assertThat(aCode.length()).as("区县/镇街码为 6 位或 12 位: " + aCode) |
|||
.isIn(6, 12); |
|||
assertThat(aCode).startsWith(cCode.substring(0, 4)); |
|||
assertThat(codes.add(aCode)).as("区县级码唯一: " + aCode).isTrue(); |
|||
} |
|||
} |
|||
} |
|||
assertThat(cities).as("市级数量").isEqualTo(342); |
|||
assertThat(areas).as("区县级数量").isEqualTo(3056); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("港澳台仅省级:810000 / 820000 / 710000 存在且无下级") |
|||
void dataFile_hkMoTwProvincesOnly() { |
|||
Map<String, JsonNode> byCode = new HashMap<>(); |
|||
for (JsonNode province : root) { |
|||
byCode.put(initializer.padded(province), province); |
|||
} |
|||
assertThat(byCode).containsKeys("810000", "820000", "710000"); |
|||
assertThat(byCode.get("810000").path("name").asText()).isEqualTo("香港特别行政区"); |
|||
assertThat(byCode.get("820000").path("name").asText()).isEqualTo("澳门特别行政区"); |
|||
assertThat(byCode.get("710000").path("children")).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("补零规则:2 位补 4 个 0,4 位补 2 个 0,6 位原样,9 位镇街码补到 12 位") |
|||
void padded_variousLengths() { |
|||
ObjectMapper mapper = new ObjectMapper(); |
|||
assertThat(initializer.padded(mapper.createObjectNode().put("code", "11"))).isEqualTo("110000"); |
|||
assertThat(initializer.padded(mapper.createObjectNode().put("code", "1101"))).isEqualTo("110100"); |
|||
assertThat(initializer.padded(mapper.createObjectNode().put("code", "110101"))).isEqualTo("110101"); |
|||
assertThat(initializer.padded(mapper.createObjectNode().put("code", "441900003"))) |
|||
.as("东莞镇街 9 位码补零到 12 位").isEqualTo("441900003000"); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("实体构建:新建无 id 挂父级;已存在复用原 id 并刷新名称") |
|||
void build_newAndExisting() { |
|||
ObjectMapper mapper = new ObjectMapper(); |
|||
JsonNode node = mapper.createObjectNode().put("code", "1101").put("name", "市辖区"); |
|||
Map<String, SysRegion> existing = new HashMap<>(); |
|||
|
|||
SysRegion created = initializer.build(node, 2, 100L, existing); |
|||
assertThat(created.getCode()).isEqualTo("110100"); |
|||
assertThat(created.getLevel()).isEqualTo(2); |
|||
assertThat(created.getParentId()).isEqualTo(100L); |
|||
assertThat(created.getId()).as("新建实体无 id,等待 upsert 插入").isNull(); |
|||
|
|||
SysRegion hit = new SysRegion(); |
|||
hit.setId(999L); |
|||
hit.setCode("110100"); |
|||
hit.setName("旧名"); |
|||
existing.put("110100", hit); |
|||
JsonNode renamed = mapper.createObjectNode().put("code", "1101").put("name", "市辖区-新"); |
|||
|
|||
SysRegion reused = initializer.build(renamed, 2, 200L, existing); |
|||
assertThat(reused).isSameAs(hit); |
|||
assertThat(reused.getId()).as("已存在实体保留原 id").isEqualTo(999L); |
|||
assertThat(reused.getName()).isEqualTo("市辖区-新"); |
|||
assertThat(reused.getParentId()).isEqualTo(200L); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue