地址选择器 地址解析器

427 阅读6分钟

四级联动 地址选择器 地址解析器 实现

(其他级别可类比参考,注:如果涉及到第五级:村级,请给village表中的code和name字段添加索引)

  • 数据来源及参考:gitee.com/modood/Admi…

  • git项目名称:Administrative-divisions-of-China

  • 从 上面地址的项目中分别获取province.json、city.json、area.json、street.json文件,使用Naticat导入到数据库中,然后新增自增的id字段

  • 重命名后分别形成 address_component_province、address_component_city、address_component_area、address_component_street四张表

  • 链接:pan.baidu.com/s/1Rb15oUBz…

  • 提取码:7133

功能一:地址选择器

  • 实现填写地址信息时所用的地址选择下拉组件功能,如下图*

image.png

功能二:地址解析器

  • 实现通过地址字符串 校验、解析出有效地址功能,常用于 地址自动补全、导入 场景

环境:mysql,navicat,idea,lombok插件及依赖,jdk8,jpa

yml配置文件:

wms:
  core:
    address: 
      provinceUnits: [省,自治区,市,特别行政区]
      cityUnits: [地区,盟,自治州,市]
      areaUnits: [县,自治县,旗,自治旗,市,区,林区,特区]
      streetUnits: [乡,民族乡,镇,街道,苏木,民族苏木,区,市]
      villageUnits: [村,社区,管理区]
    exclude: [香港,澳门,台湾]

yml配置对应的javaBean配置:

@Data
@Component
@ConfigurationProperties(prefix = "wms.core.address")
public class AddressUnitsProperties {
    /**
     * 省一级行政单位集合
     */
    private List<String> provinceUnits;

    /**
     * 地二级行政单位集合
     */
    private List<String> cityUnits;

    /**
     * 县三级行政单位集合
     */
    private List<String> areaUnits;

    /**
     * 乡(镇)四级行政单位集合
     */
    private List<String> streetUnits;

    /**
     * 不支持 港澳台
     */
    private List<String> exclude;
}

地址选择器入参:

@Data
@Builder
@ApiModel(value = "地址选择器入参")
@AllArgsConstructor
@NoArgsConstructor
public class AddressReqDTO {

    /**
     * 要查找的层级
     * 省一级:1
     * 市二级:2
     * 县三级:3
     * 镇四级:4
     * @see AddressComponentEnum
     */
    @ApiModelProperty(value = "要查找的层级,省:1,市:2,县:3,镇:4", required = true)
    @NotNull
    private Integer level;

    @ApiModelProperty(value = "查找关键字")
    private String keyword;

    @ApiModelProperty(value = "省 编码")
    private Integer provinceCode;

    @ApiModelProperty(value = "市 编码")
    private Integer cityCode;

    @ApiModelProperty(value = "县/区 编码")
    private Integer areaCode;

}

地址选择器出参:

/**
 * 地址选择器响应体
 */
@Data
@Builder
@ApiModel
@AllArgsConstructor
@NoArgsConstructor
public class AddressRespDTO {

    /**
     * 查找层级
     * 省一级:1
     * 市二级:2
     * 县三级:3
     * 镇四级:4
     * @see AddressComponentEnum
     */
    private Integer level;

    /**
     * 省出参
     */
    private List<AddressComponentProvince> provinceList;

    /**
     * 市出参
     */
    private List<AddressComponentCity> cityList;

    /**
     * 县/区 出参
     */
    private List<AddressComponentArea> areaList;

    /**
     * 乡镇/街道 出参
     */
    private List<AddressComponentStreet> streetList;
}

用户输入的地址字符串被正则表达式解析后的对象:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AddressKeywordDTO {

    /**
     * 省
     */
    private String province;

    /**
     * 市
     */
    private String city;

    /**
     * 县
     */
    private String area;

    /**
     * 镇
     */
    private String street;
}

地址解析入参:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AddressResolveReq {

    /**
     * 用户手输地址
     */
    @NotBlank
    private String address;

}

地址解析出参:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddressDTO {

    /**
     * 省编码(国家行政代码)
     */
    private Integer provinceCode;

    /**
     * 省名称
     */
    private String provinceName;

    /**
     * 市编码(国家行政代码)
     */
    private Integer cityCode;

    /**
     * 市名称
     */
    private String cityName;

    /**
     * 县编码(国家行政代码)
     */
    private Integer areaCode;

    /**
     * 县名称
     */
    private String areaName;

    /**
     * 镇编码(国家行政代码)
     */
    private Integer streetCode;

    /**
     * 镇名称
     */
    private String streetName;
}

controller层:

@Slf4j
@Api(tags = "地址选择器")
@RestController
@RequestMapping("/address")
@RequiredArgsConstructor
public class AddressController {

    private final IAddressService addressService;

    @ApiOperation(value = "地址查询")
    @PostMapping(value = "/addressSelector")
    public ApiEntity<AddressRespDTO> addressSelector(@ApiParam("请求体") @RequestBody @Valid AddressReqDTO addressReqDTO) {
        addressService.validateAddressParam(addressReqDTO);
        return ApiEntity.ok(addressService.addressSelector(addressReqDTO));
    }
    
    @ApiOperation(value = "地址解析")
    @PostMapping(value = "/addressResolver")
    public ApiEntity<AddressRespDTO> addressSelector(@ApiParam("请求体") @RequestBody @Valid AddressReqDTO addressReqDTO) {
        addressService.validateAddressParam(addressReqDTO);
        return ApiEntity.ok(addressService.addressResolver(addressReqDTO));
    }
}

service接口层:

public interface IAddressService {

    /**
     * 地址选择器入参校验
     * @param addressReqDTO
     */
    void validateAddressParam(AddressReqDTO addressReqDTO);
    
    /**
     * 地址选择器
     */
    AddressRespDTO addressSelector(AddressReqDTO addressReqDTO);


    /********************上面地址选择器相关方法,下面地址解析器相关方法********************/


    /**
     * 是否存在不支持地址(暂不支持港澳台)
     * @param address 地址
     * @return true:存在 false:不存在
     */
    boolean existExcludeAddress(String address);

    /**
     * 根据输入的地址字符串,提取相应的 省、市、县、镇 四级行政单位地址
     * @param address 地址
     * @return
     */
    List<Map<String, String>> addressResolution(String address);

    /**
     * 去除区域行政单位,得到搜索关键字
     * 逻辑:只匹配替换最后的指定字符串,只对长度大于2的各级地址做截取
     * @param addressKeywordDTO
     */
    AddressKeywordDTO removeAdministrativeUnit(AddressKeywordDTO addressKeywordDTO);

    /**
     * 通过地址获取相应的数据库记录
     */
    List<AddressDTO> getStreetsByAddress(AddressKeywordDTO addressKeywordDTO);
    
    /**
     * 地址解析器
     * @param address 要被解析的地址
     * @return 被解析后的地址
     */
    AddressDTO addressResolver(String address);
}

service实现层:

@Service
@Slf4j
public class AddressServiceImpl implements IAddressService {

    @Autowired
    private AddressUnitsProperties addressUnitsProperties;

    @Autowired
    private AddressComponentProvinceRepository provinceRepository;

    @Autowired
    private AddressComponentCityRepository cityRepository;

    @Autowired
    private AddressComponentAreaRepository areaRepository;

    @Autowired
    private AddressComponentStreetRepository streetRepository;
    
    /**
     * 对地址组件的查询入参做校验
     * @param addressReqDTO
     */
    @Override
    public void validateAddressParam(AddressReqDTO addressReqDTO) {
        if (Objects.isNull(addressReqDTO.getLevel())){
            throw new ApiException(ErrorCode.DATA_VALIDATE_ERROR);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.CITY.getLevel() && Objects.isNull(addressReqDTO.getProvinceCode())){
            throw new ApiException(ErrorCode.DATA_VALIDATE_ERROR);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.AREA.getLevel()
                && (Objects.isNull(addressReqDTO.getProvinceCode()) || Objects.isNull(addressReqDTO.getCityCode()))){
            throw new ApiException(ErrorCode.DATA_VALIDATE_ERROR);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.STREET.getLevel()
                && (Objects.isNull(addressReqDTO.getProvinceCode()) || Objects.isNull(addressReqDTO.getCityCode()) || Objects.isNull(addressReqDTO.getAreaCode()))){
            throw new ApiException(ErrorCode.DATA_VALIDATE_ERROR);
        }
    }

    /**
     * 地址选择器
     */
    @Override
    public AddressRespDTO addressSelector(AddressReqDTO addressReqDTO) {
        AddressRespDTO addressRespDTO = new AddressRespDTO();
        addressRespDTO.setLevel(addressReqDTO.getLevel());

        if (addressReqDTO.getLevel() == AddressComponentEnum.PROVINCE.getLevel()){ //查询 省份
            List<AddressComponentProvince> provinceList = StringUtils.isBlank(addressReqDTO.getKeyword())
                    ? provinceRepository.findAll()
                    : provinceRepository.findByNameLike(QueryUtils.addLikeChar(addressReqDTO.getKeyword()));
            addressRespDTO.setProvinceList(provinceList);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.CITY.getLevel()){ //查询 城市
            List<AddressComponentCity> cityList = StringUtils.isBlank(addressReqDTO.getKeyword())
                    ? cityRepository.findByProvinceCode(addressReqDTO.getProvinceCode())
                    : cityRepository.findByProvinceCodeAndNameLike(addressReqDTO.getProvinceCode(),QueryUtils.addLikeChar(addressReqDTO.getKeyword()));
            addressRespDTO.setCityList(cityList);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.AREA.getLevel()){ //查询 区县
            List<AddressComponentArea> areaList = StringUtils.isBlank(addressReqDTO.getKeyword())
                    ? areaRepository.findByProvinceCodeAndCityCode(addressReqDTO.getProvinceCode(), addressReqDTO.getCityCode())
                    : areaRepository.findByProvinceCodeAndCityCodeAndNameLike(addressReqDTO.getProvinceCode(), addressReqDTO.getCityCode(),QueryUtils.addLikeChar(addressReqDTO.getKeyword()));
            addressRespDTO.setAreaList(areaList);
        }else if (addressReqDTO.getLevel() == AddressComponentEnum.STREET.getLevel()){ //查询 乡镇
            List<AddressComponentStreet> streetList = StringUtils.isBlank(addressReqDTO.getKeyword())
                    ? streetRepository.findByProvinceCodeAndCityCodeAndAreaCode(addressReqDTO.getProvinceCode(), addressReqDTO.getCityCode(), addressReqDTO.getAreaCode())
                    : streetRepository.findByProvinceCodeAndCityCodeAndAreaCodeAndNameLike(addressReqDTO.getProvinceCode(), addressReqDTO.getCityCode(), addressReqDTO.getAreaCode(),QueryUtils.addLikeChar(addressReqDTO.getKeyword()));
            addressRespDTO.setStreetList(streetList);
        }
        return addressRespDTO;
    }


/********************上面地址选择器相关方法,下面地址解析器相关方法********************/


    /**
     * 是否存在不支持地址(暂不支持港澳台)
     * @param address 地址
     * @return true:存在 false:不存在
     */
    @Override
    public boolean existExcludeAddress(String address) {
        List<String> excludeAddressKeys = addressUnitsProperties.getExclude();
        if (!CollectionUtils.isEmpty(excludeAddressKeys)){
            for (String excludeAddressKey : excludeAddressKeys){
                //判断是否以不支持的关键字开头
                if (address.matches("^" + excludeAddressKey + ".*")){
                    return true;
                }
            }
        }
       return false;
    }

    /**
     * 根据输入的地址字符串,提取相应的 省、市、县、镇 四级行政单位地址
     * @param address 地址
     * @return
     */
    public List<Map<String, String>> addressResolution(String address) {
        String regex="(?<province>.+?省|.+?自治区|.+?市|.+?特别行政区)(?<city>.+?地区|.+?盟|.+?自治州|.+?市)(?<county>.+?县|.+?自治县|.+?旗|.+?自治旗|.+?市|.+?区|.+?林区|.+?特区)(?<town>.+?乡|.+?民族乡|.+?镇|.+?街道|.+?苏木|.+?民族苏木|.+?区|.+?市)(?<village>.*?)";
        Matcher m= Pattern.compile(regex).matcher(address);
        String province=null,city=null,county=null,town=null,village=null;
        List<Map<String,String>> table=new ArrayList<Map<String,String>>();
        Map<String,String> row=null;
        while(m.find()){
            row=new LinkedHashMap<String,String>();
            province=m.group("province");
            row.put("province", province==null?"":province.trim());
            city=m.group("city");
            row.put("city", city==null?"":city.trim());
            county=m.group("county");
            row.put("area", county==null?"":county.trim());
            town=m.group("town");
            row.put("street", town==null?"":town.trim());
            village=m.group("village");
            row.put("village", village==null?"":village.trim());
            table.add(row);
        }
        return table;
    }

    /**
     * 去除区域行政单位,得到搜索关键字
     * 逻辑:只匹配替换最后的指定字符串,只对长度大于2的各级地址做截取
     * @param addressKeywordDTO
     */
    public AddressKeywordDTO removeAdministrativeUnit(AddressKeywordDTO addressKeywordDTO){
        int length = 2;
        addressUnitsProperties.getProvinceUnits().stream().forEach(provinceUnit -> {
            Optional.ofNullable(addressKeywordDTO.getProvince()).filter(provinceKeyword -> provinceKeyword.length() > length).ifPresent(provinceKeyword -> {
                addressKeywordDTO.setProvince(provinceKeyword.replaceAll(provinceUnit + "$", ""));
            });
        });
        addressUnitsProperties.getCityUnits().stream().forEach(cityUnit -> {
            Optional.ofNullable(addressKeywordDTO.getCity()).filter(cityKeyword -> cityKeyword.length() > length).ifPresent(cityKeyword -> {
                addressKeywordDTO.setCity(cityKeyword.replaceAll(cityUnit+"$", ""));
            });
        });
        addressUnitsProperties.getAreaUnits().stream().forEach(areaUnit -> {
            Optional.ofNullable(addressKeywordDTO.getArea()).filter(areaKeyword -> areaKeyword.length() > length).ifPresent(areaKeyword -> {
                addressKeywordDTO.setArea(areaKeyword.replaceAll(areaUnit+"$", ""));
            });
        });
        addressUnitsProperties.getStreetUnits().stream().forEach(streetUnit -> {
            Optional.ofNullable(addressKeywordDTO.getStreet()).filter(streetKeyword -> streetKeyword.length() > length).ifPresent(streetKeyword -> {
                addressKeywordDTO.setStreet(streetKeyword.replaceAll(streetUnit+"$", ""));
            });
        });
        return addressKeywordDTO;
    }

    /**
     * 通过地址获取相应的数据库记录
     * @param addressKeywordDTO
     * @return
     */
    @Override
    public List<AddressDTO> getStreetsByAddress(AddressKeywordDTO addressKeywordDTO) {
        List<AddressDTO> addressDTOS = new ArrayList<>();

        //省
        List<AddressComponentProvince> provinceList = provinceRepository.findByNameLike(QueryUtils.addLikeChar(addressKeywordDTO.getProvince()));

        if (!CollectionUtils.isEmpty(provinceList)){
            //市
            Map<Integer, AddressComponentProvince> provinceMap = provinceList.stream().collect(Collectors.toMap(AddressComponentProvince::getCode, Function.identity()));
            List<AddressComponentCity> cityList = cityRepository.findByProvinceCodeInAndNameLike(provinceMap.keySet(), QueryUtils.addLikeChar(addressKeywordDTO.getCity()));

            if (!CollectionUtils.isEmpty(cityList)){
                //县
                Map<Integer, AddressComponentCity> cityMap = cityList.stream().collect(Collectors.toMap(AddressComponentCity::getCode,Function.identity()));
                List<AddressComponentArea> areaList = areaRepository.findByCityCodeInAndNameLike(cityMap.keySet(), QueryUtils.addLikeChar(addressKeywordDTO.getArea()));

                if (!CollectionUtils.isEmpty(areaList)){
                    //镇
                    Map<Integer, AddressComponentArea> areaMap = areaList.stream().collect(Collectors.toMap(AddressComponentArea::getCode, Function.identity()));
                    List<AddressComponentStreet> streetList = streetRepository.findByAreaCodeInAndNameLike(areaMap.keySet(), QueryUtils.addLikeCharSuffix(addressKeywordDTO.getStreet()));

                    if (!CollectionUtils.isEmpty(streetList)){
                        //数据封装
                        streetList.stream().forEach(street -> {
                            AddressComponentProvince province = provinceMap.get(street.getProvinceCode());
                            AddressComponentCity city = cityMap.get(street.getCityCode());
                            AddressComponentArea area = areaMap.get(street.getAreaCode());

                            AddressDTO addressDTO = AddressDTO.builder()
                                    .provinceCode(province.getCode()).provinceName(province.getName())
                                    .cityCode(city.getCode()).cityName(city.getName())
                                    .areaCode(area.getCode()).areaName(area.getName())
                                    .streetCode(street.getCode()).streetName(street.getName()).build();

                            addressDTOS.add(addressDTO);
                        });
                    }
                }
            }
        }
        return addressDTOS;
    }
}

/**
 * 地址解析器
 * @param address 要被解析的地址
 * @return 被解析后的地址
 */
@Override
public AddressDTO addressResolver(String address) {
    //校验 港澳台
    if(existExcludeAddress(addressResolveReq.getAddress())){
        log.error("暂不支持港澳台");
        throw new ApiException(ErrorCode.ADDRESS_IS_EXCLUDE);
    }

    List<Map<String,String>> table = addressResolution(address);
    if (CollectionUtils.isEmpty(table) || table.size() != 1){
        log.error("无法提取有效采样地址:{}",address);
        throw new ApiException(ErrorCode.ADDRESS_IS_INVALID);
    }

    //构建地址提取对象
    AddressKeywordDTO addressKeywordDTO = AddressKeywordDTO.builder()
            .province(table.get(0).get("province"))//省
            .city(table.get(0).get("city"))//市
            .area(table.get(0).get("area"))//区、县
            .street(table.get(0).get("street")).build();//乡、镇

    if (StringUtils.isBlank(addressKeywordDTO.getProvince())
            || StringUtils.isBlank(addressKeywordDTO.getCity())
            || StringUtils.isBlank(addressKeywordDTO.getArea())
            || StringUtils.isBlank(addressKeywordDTO.getStreet())){
        log.error("无法提取有效采样地址:{}",address);
        throw new ApiException(ErrorCode.ADDRESS_IS_INVALID);
    }

    //去除区域行政单位,得到搜索关键字放入数据库中查找更准确
    removeAdministrativeUnit(addressKeywordDTO);

    //校验 省、市、县、镇 四级行政单位 ,用来判断是否存在 唯一性、有效性
    List<AddressDTO> addressDTOS = getStreetsByAddress(addressKeywordDTO);

    if (CollectionUtils.isEmpty(addressDTOS)){
        log.error("地址不存在:{}",address);
        throw new ApiException(ErrorCode.ADDRESS_IS_NOT_EXIST);
    }else if (addressDTOS.size() != 1){
        log.error("无法定位到唯一地址:{}",address);
        throw new ApiException(ErrorCode.ADDRESS_IS_NOT_UNIQUE);
    }
    return addressDTOS.get(0);
}

省一级的持久层:

@Repository
public interface AddressComponentProvinceRepository extends BaseRepository<AddressComponentProvince,Integer> {

    List<AddressComponentProvince> findByNameLike(String name);

}

地市二级的持久层:

@Repository
public interface AddressComponentCityRepository extends BaseRepository<AddressComponentCity,Integer> {

    List<AddressComponentCity> findByProvinceCode(Integer provinceCode);

    List<AddressComponentCity> findByProvinceCodeAndNameLike(Integer provinceCode,String name);

    List<AddressComponentCity> findByProvinceCodeInAndNameLike(Set<Integer> provinceCodeList, String name);
}

区、县三级的持久层:

@Repository
public interface AddressComponentAreaRepository extends BaseRepository<AddressComponentArea,Integer> {

    List<AddressComponentArea> findByProvinceCodeAndCityCode(Integer provinceCode,Integer cityCode);

    List<AddressComponentArea> findByProvinceCodeAndCityCodeAndNameLike(Integer provinceCode,Integer cityCode,String name);

    List<AddressComponentArea> findByCityCodeInAndNameLike(Set<Integer> cityCodeList, String addLikeChar);
}

乡镇、街道四级的持久层:

@Repository
public interface AddressComponentStreetRepository extends BaseRepository<AddressComponentStreet,Integer> {

    List<AddressComponentStreet> findByProvinceCodeAndCityCodeAndAreaCode(Integer provinceCode,Integer cityCode,Integer areaCode);

    List<AddressComponentStreet> findByProvinceCodeAndCityCodeAndAreaCodeAndNameLike(Integer provinceCode,Integer cityCode,Integer areaCode,String name);

    List<AddressComponentStreet> findByCodeIn(Collection<Integer> codes);

    List<AddressComponentStreet> findByAreaCodeInAndNameLike(Set<Integer> areaCodeList, String addLikeChar);
}