Browse Source

feat: 行政区划全量种子导入——RegionDataInitializer 幂等导入国标三级 3432 行(frontend-integration 票据 01)

master
luoweijian 3 weeks ago
parent
commit
a644f8db13
  1. 143
      crm-rule/src/main/java/com/crm/rule/config/RegionDataInitializer.java
  2. 7
      crm-rule/src/main/java/com/crm/rule/service/ISysRegionService.java
  3. 1
      crm-rule/src/main/resources/data/pca-code.json
  4. 121
      crm-rule/src/test/java/com/crm/rule/config/RegionDataInitializerTest.java

143
crm-rule/src/main/java/com/crm/rule/config/RegionDataInitializer.java

@ -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());
}
}

7
crm-rule/src/main/java/com/crm/rule/service/ISysRegionService.java

@ -1,14 +1,17 @@
package com.crm.rule.service;
import com.crm.base.service.IBaseService;
import com.crm.rule.domain.dto.SysRegionDTO;
import com.crm.rule.domain.entity.SysRegion;
import java.util.Collection;
import java.util.List;
/**
* 行政区划查询服务只读数据由 CSV 一次性导入
* 行政区划查询服务只读查询接口种子导入走 {@link IBaseService#batchUpsert}
* RegionDataInitializer 幂等全量导入
*/
public interface ISysRegionService {
public interface ISysRegionService extends IBaseService<SysRegion> {
/** 按上级ID查子区划(parentId=null 查省级) */
List<SysRegionDTO> listByParent(Long parentId);

1
crm-rule/src/main/resources/data/pca-code.json

File diff suppressed because one or more lines are too long

121
crm-rule/src/test/java/com/crm/rule/config/RegionDataInitializerTest.java

@ -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…
Cancel
Save