Skip to content

地区与 IP 库 ​

这篇解决什么 ​

省市区下拉、收货地址、登录 IP 落城市,都读同一套内存库,不建地区表。读完能改 AreaUtils / IPUtils、area.csv / ip2region.xdb,以及 /admin-api/system/area。

默认端口 48080,管理端前缀 /admin-api。菜单在系统管理 → 地区管理,路由 /system/area。

示意图:页面只调地区接口;省市区在 AreaUtils,IP 先查 xdb 再回到 Area。

代码位置 ​

名称说明仓库路径
IP Starter启动时装 CSV 和 xdb 进内存ruoyi-office/yudao-framework/yudao-spring-boot-starter-biz-ip/
管理端接口中国地区树、按 IP 查城市yudao-module-system-server/.../admin/ip/AreaController.java
用户端接口同样出树,@PermitAll.../controller/app/ip/AppAreaController.java
管理端页面树表 +「IP 查询」弹窗ruoyi-office-vben/apps/web-antd/src/views/system/area/
前端 APIgetAreaTree / getCityAreaTree / getAreaByIpweb-antd/src/api/system/area/index.ts
级联组件表单里绑一个 areaIdweb-antd/src/components/area/area-cascader.vue
菜单id 2083,组件 system/area/indexsystem_menu

system、CRM、HRM、会员、商城交易都直接依赖这个 Starter。单体入口随 yudao-module-system-server 带上,不必在 yudao-server 再写一行。

地区 ​

area.csv 大约 3879 行(含表头共 3880 行),列是 id,name,type,parentId。AreaUtils 静态块读完后挂父子,缓存在 Map<Integer, Area>。根节点是编号 0 的「全球」,中国是 1。

注释里的行政区划来源是 Administrative-divisions-of-China。改库只改这份 CSV,不要另做一张 system_area。

名称说明仓库路径
Area内存节点:id / name / type / parent / children.../ip/core/Area.java
ID_GLOBAL0,全球根同上
ID_CHINA1,中国同上
AreaTypeEnum1 国家、2 省份、3 城市、4 区县.../ip/core/enums/AreaTypeEnum.java
area.csv启动只读一次.../starter-biz-ip/src/main/resources/area.csv

Area 没有 parentId 字段。CSV 要扫两遍:先建节点,再按第 4 列挂到父节点。

地区管理树表

本系统截图:系统管理 → 地区管理,树根是省级(含直辖市、港澳)。

接口只走中国子树,国外国家行在 CSV 里,但 /tree 不会列出来。

名称说明仓库路径
GET /system/area/treeAreaUtils.getChinaAreaTree(maxType),再转 AreaNodeRespVOAreaController
maxType1~4,按 AreaTypeEnum 截断;薪酬城市传 3同上;前端 getCityAreaTree()
AreaNodeRespVOid / name / type / children,不含 parent.../admin/ip/vo/AreaNodeRespVO.java
App /system/area/tree同样的树,免登录AppAreaController
java
List<Area> tree = AreaUtils.getChinaAreaTree(maxTypeEnum);
return success(BeanUtils.toBean(tree, AreaNodeRespVO.class));

getChinaAreaTree 会拷一份树。maxType 等于当前节点类型时不再挂孩子,所以 maxType=3 只到地级市。

AreaUtils ​

名称说明仓库路径
getArea(id)按编号取节点.../ip/core/utils/AreaUtils.java
format(id)从当前节点拼到省,空格分隔同上
format(id, "/")自定义分隔符,给 Excel、展示用同上
parseArea("省/市/区")按 / 逐级找孩子同上
getByType按类型滤全表,例如全部省份同上
getParentIdByType朝阳区向上找到省 110000同上
getAreaName只要本节点名字同上
getAreaNodePathList展开成「省/市/区」路径列表同上

format 遇到父节点是全球或中国就停,所以 format(1) 是「中国」,format(110105) 是「北京市 北京市 朝阳区」,前面不带「中国」。编号不存在返回 null。单测在 AreaUtilsTest。

java
AreaUtils.getArea(110100);                 // 北京市,类型城市
AreaUtils.format(110105);                  // 北京市 北京市 朝阳区
AreaUtils.format(110105, "/");             // 北京市/北京市/朝阳区
AreaUtils.parseArea("北京市/北京市/朝阳区"); // id = 110105
AreaUtils.getParentIdByType(110105, AreaTypeEnum.PROVINCE); // 110000

业务表只存 areaId

CRM 客户、会员地址、商城收货地址都存整数编号,展示时再 AreaUtils.format。不要把「省市区」字符串写进业务表当主键。

会员地址 Convert 就是这样回填名称:

java
@Named("convertAreaIdToAreaName")
default String convertAreaIdToAreaName(Integer areaId) {
    return AreaUtils.format(areaId);
}

这段在 yudao-module-member-server 的 AddressConvert。CRM 列表同样调用 AreaUtils.format(customerVO.getAreaId())。

操作日志函数名 getArea 也走 format,类是 AreaParseFunction。

前端怎么选 ​

名称说明仓库路径
AreaCascader拉完整树,v-model 只回写最后一级 areaIdweb-antd/src/components/area/
表单设计器 AreaSelectlevel 用 AreaLevelEnum 控制深度web-antd/src/components/form-create/components/area-select.vue
薪酬城市getCityAreaTree(),maxType=3views/hrm/salary/_shared/form-helpers.ts
移动端同一套 /system/area/treeruoyi-office-uniapp/src/api/system/area/index.ts

CRM 客户 / 线索 / 联系人、会员用户、商城订单和自提门店的表单都挂了 AreaCascader。

两套「层级」数字不一样

后端 AreaTypeEnum:国家 1、省 2、市 3、区 4。前端 AreaLevelEnum:省 1、市 2、区 3,只给表单设计器截层级。getAreaTree(maxType) 必须传后端那套,薪酬城市传 3 才是市。

IP ​

IPUtils 启动时把 ip2region.xdb(约 4MB)读进 Searcher.newWithBuffer。依赖是 org.lionsoul:ip2region 2.7.0。这份 xdb 的检索结果是地区编号字符串,不是默认的「国家|省|市」文本,所以能直接 Integer.parseInt 再进 AreaUtils。

名称说明仓库路径
ip2region.xdb精简库,检索值对齐 Area.id.../starter-biz-ip/src/main/resources/ip2region.xdb
getAreaId(ip)127.0.0.1 这种点分地址.../ip/core/utils/IPUtils.java
getAreaId(long)Searcher.checkIP 转成的长整型同上
getArea(...)先拿编号,再 AreaUtils.getArea同上
单测120.202.4.50 → 420600 襄阳市IPUtilsTest
java
Integer areaId = IPUtils.getAreaId("120.202.4.50"); // 420600
Area area = IPUtils.getArea("120.202.4.50");        // 襄阳市

管理端「IP 查询」调 GET /system/area/get-by-ip?ip=。查不到返回「未知」,查到则 AreaUtils.format。

IP 查询弹窗

本系统截图:地区管理点「IP 查询」,输入公网地址后地址栏回填格式化地名。

登录日志、操作日志里的 userIp 只是字符串,来自 ServletUtils.getClientIP(),不会自动转城市。要落地区,自己再调 IPUtils.getArea(ip)。取客户端 IP 见 工具类。

内网 IP 不要当城市

127.0.0.1、10.*、192.168.* 在 xdb 里经常对不上正式区划,getArea 会得到 null,接口就返回「未知」。演示请用单测里的公网段。

导入导出 ​

Excel 读「北京市/北京市/朝阳区」这种路径时,用 AreaConvert 调 parseArea,再把 area.getId() 转成字段类型。路径对不上会打 error 日志并得到 null。

名称说明仓库路径
AreaConvert单元格文字 → areaIdyudao-spring-boot-starter-excel/.../convert/AreaConvert.java
CRM 下拉getAreaNodePathList(中国.children).../crm/.../AreaExcelColumnSelectFunction.java

模板列必须用 / 拼接,和 format(id, "/") 一致。空格版 format(id) 解析不了。导入导出总览见 Excel 导入导出。

扩展时注意 ​

改行政区划:替换 area.csv 后跑 AreaUtilsTest。直辖市会同时出现「北京市 / 北京市」,format 会打出两个「北京市」,这是数据如此,不要当成 bug 去重。

改 IP 库:xdb 的值必须仍是 Area.id。换官方原版 ip2region 库会把 Integer.parseInt 直接打崩。两份资源都随 Starter 打进 jar,更新后要重新编译该模块。

AreaUtils / IPUtils 都在静态块里 init,失败会抛 RuntimeException 让进程起不来。CSV 缺列、父子 id 相同、xdb 读不到,都属于启动失败,不是业务空数据。

统计按省聚合时,先 getParentIdByType(areaId, PROVINCE),再对 getByType(PROVINCE) 做补零。CRM 客户画像、会员地区统计都是这个写法。

配置与操作 ​

管理员在 系统管理 → 地区管理 浏览树,没有单独的「导入地区」页。数据在 yudao-spring-boot-starter-biz-ip 启动时从 CSV / xdb 载入内存。

改库文件要换包并重启,页面保存不会热更新 IP 库。查询 IP 走 GET /system/area 的按 IP 接口。开启见 系统功能设计。截图见上文列表和查询。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版