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