34 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 6350 | 行政区划主数据与三级联动 | admin | lc(GIT) | 新增接口 | deployed | verified | pending | 主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约。 | 2026-08-26 | dev-v3 |
User: 行政区划主数据与三级联动
服务:
hl-user-service(统一经 Gateway 访问)PR: #6359、#6387、#6393、#6398、#6402、#6412
Issue: #6350、#6384、#6389、#6395、#6399、#6409
日期: 2026-08-26
影响范围: 管理后台行政区划维护、省/市/县三级选择器和历史值父链回显
⚠️ 关键变化
- 本次补文档不改变 TEST 上已经部署的接口行为;它把原 Changelog 中分散的说明整理为可直接联调的完整契约。
- 前端不得再把行政区划 ID 当 JavaScript number:响应中的所有行政区划 ID 均为 JSON string,缺失层级保持真正的
null。 - 三级联动不允许通过行政区编码截位推断父子关系;必须逐级调用
children,编辑回显先调用path。 - 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体
code、success、message和data。
一、背景
管理后台需要一套统一的行政区划主数据能力,同时支持:
- 省、市、县三级选择器逐级加载;
- 编辑历史业务数据时,从任意节点恢复完整父链;
- 具有专用权限的管理员创建自定义行政区或导航聚合节点;
- 使用
rowVersion防止两位管理员并发更新时互相覆盖。
接口当前只开放省、市、县三级写入,层级代码依次为 PROVINCE、PREFECTURE、COUNTY。接口返回的示例 ID 仅用于展示字符串类型,前端不得硬编码。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 单级联动列表 | GET | /admin/region/children |
新增接口 | 加载根层省级或指定父节点的当前有效直接子节点 |
| 2 | 父链回显 | GET | /admin/region/{id}/path |
新增接口 | 从任意省/市/县节点恢复省级根到目标节点的完整路径 |
| 3 | 创建行政区划节点 | POST | /admin/region |
新增接口 | 创建自定义行政区或不可选择的导航聚合节点 |
| 4 | 更新行政区划节点 | PUT | /admin/region/{id} |
新增接口 | 使用完整可变字段和 rowVersion 执行 CAS 更新 |
三、接口详情
1. 单级联动列表 GET /admin/region/children
VO: AdministrativeRegionChildrenVO / AdministrativeRegionItemVO
使用场景
- 新建页面首次加载省级列表:省略
parentId。 - 选择省后加载市、选择市后加载县:把当前选中节点 ID 作为下一次请求的
parentId。 - 编辑页面逐层恢复默认值:把本层历史 ID 作为
selectedId;它仍在当前items中时会成为defaultId。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
Authorization |
Header | String | ✅ | Bearer <token> |
真实管理端登录令牌 |
parentId |
Query | String | ❌ | 十进制正整数;根层省级列表必须省略 | 指定要查询直接子节点的父节点 ID |
selectedId |
Query | String | ❌ | 十进制正整数 | 编辑回显时希望优先选中的本级节点 ID |
无请求体。
出参 Result<AdministrativeRegionChildrenVO>
| 字段 | 类型 | 说明 |
|---|---|---|
code |
Integer | 200 表示成功;业务失败读取具体业务码 |
message |
String | 响应说明 |
success |
Boolean | 仅当 code=200 时为 true |
traceId |
String / null | 链路追踪 ID;报错排查时提供给后端 |
data.parentId |
String / null | 本次查询父节点;根层查询为 null |
data.defaultId |
String / null | 合法 selectedId,否则为排序后首项 ID;空列表为 null |
data.items |
Array | 当前有效且状态为 ACTIVE 的直接子节点,稳定排序 |
data.items[].id |
String | 节点 ID,始终为字符串 |
data.items[].parentId |
String / null | 父节点 ID;省级根节点为 null |
data.items[].codeStandard |
String | 编码标准,例如 GB/T 2260、HL_CUSTOM、HL_GROUP |
data.items[].regionCode |
String | 编码标准内的地区代码 |
data.items[].levelCode |
String | PROVINCE、PREFECTURE 或 COUNTY |
data.items[].depth |
Integer | 树深度,省/市/县分别为 1/2/3 |
data.items[].name |
String | 行政区划完整名称 |
data.items[].shortName |
String / null | 简称 |
data.items[].nodeKind |
String | ADMINISTRATIVE 或 AGGREGATION |
data.items[].navigable |
Boolean | 是否允许继续查询下一层 |
data.items[].selectable |
Boolean | 是否允许作为最终业务行政区值 |
data.items[].hasChildren |
Boolean | 是否存在当前有效、启用的直接子节点 |
data.items[].currentlyEffective |
Boolean | 中国业务当日是否位于有效期且状态启用 |
data.items[].status |
String | ACTIVE 或 INACTIVE |
data.items[].validFrom |
String | 生效日期,格式 yyyy-MM-dd |
data.items[].validTo |
String / null | 失效日期;null 表示持续有效 |
data.items[].sortOrder |
Integer | 同级排序号 |
data.items[].rowVersion |
Integer | 当前 CAS 版本,更新时原样提交 |
请求示例
GET /admin/region/children?parentId=2090000000000000001&selectedId=2090000000000000035 HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <管理员令牌>
Accept: application/json
根层省级列表:
GET /admin/region/children HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <管理员令牌>
Accept: application/json
响应示例
{
"code": 200,
"message": "成功",
"data": {
"parentId": "2090000000000000001",
"items": [
{
"id": "2090000000000000035",
"parentId": "2090000000000000001",
"codeStandard": "GB/T 2260",
"regionCode": "110100",
"levelCode": "PREFECTURE",
"depth": 2,
"name": "北京市",
"shortName": null,
"nodeKind": "ADMINISTRATIVE",
"navigable": true,
"selectable": true,
"hasChildren": true,
"currentlyEffective": true,
"status": "ACTIVE",
"validFrom": "2025-12-31",
"validTo": null,
"sortOrder": 100,
"rowVersion": 0
}
],
"defaultId": "2090000000000000035"
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
空数据 / 降级响应
父节点存在但没有当前有效直接子节点时,返回成功空数组:
{
"code": 200,
"message": "成功",
"data": {
"parentId": "2090000000000000035",
"items": [],
"defaultId": null
},
"success": true
}
错误响应
父节点不存在或已删除:
{
"code": 210801,
"message": "行政区划节点不存在",
"data": null,
"success": false
}
业务边界
- 只返回
status=ACTIVE且当前有效的直接子节点,停用或历史节点不会混入当前选择项。 selectedId不在本次items中时不会报错,而是稳定回退到排序后的第一项;空列表回退为null。- 排序固定为
sortOrder、regionCode、id,前端不得另用行政区编码推断顺序或父子关系。 navigable控制能否继续向下加载,selectable控制能否作为最终值;两者不能相互替代。- 未登录返回业务码
401;parentId=0等非正整数参数返回业务码400。
2. 父链回显 GET /admin/region/{id}/path
VO: AdministrativeRegionPathVO / AdministrativeRegionItemVO
使用场景
编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整省/市/县父链,再按父链逐层调用 children 加载每一级可选项。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
Authorization |
Header | String | ✅ | Bearer <token> |
真实管理端登录令牌 |
id |
Path | String | ✅ | 十进制正整数 | 要回显的省、市或县节点 ID |
无请求体。
出参 Result<AdministrativeRegionPathVO>
| 字段 | 类型 | 说明 |
|---|---|---|
code |
Integer | 200 表示成功;业务失败读取具体业务码 |
message |
String | 响应说明 |
success |
Boolean | 仅当 code=200 时为 true |
data.selectedId |
String | 调用方传入并成功解析的目标节点 ID |
data.provinceId |
String | 父链中的省级 ID |
data.prefectureId |
String / null | 父链中的地级 ID;目标为省级时为 null |
data.countyId |
String / null | 父链中的县级 ID;目标高于县级时为 null |
data.path |
Array | 从省级根到目标节点的完整有序父链 |
data.path[].id |
String | 节点 ID,始终为字符串 |
data.path[].parentId |
String / null | 父节点 ID;首个省级节点为 null |
data.path[].codeStandard |
String | 编码标准 |
data.path[].regionCode |
String | 地区代码 |
data.path[].levelCode |
String | 省/市/县层级代码 |
data.path[].depth |
Integer | 从 1 开始的连续深度 |
data.path[].name |
String | 完整名称 |
data.path[].shortName |
String / null | 简称 |
data.path[].nodeKind |
String | 节点类型 |
data.path[].navigable |
Boolean | 是否允许逐级导航 |
data.path[].selectable |
Boolean | 是否允许作为最终值 |
data.path[].hasChildren |
Boolean | 是否存在当前有效直接子节点 |
data.path[].currentlyEffective |
Boolean | 当前是否有效;历史节点可能为 false |
data.path[].status |
String | ACTIVE 或 INACTIVE |
data.path[].validFrom |
String | 生效日期 |
data.path[].validTo |
String / null | 失效日期 |
data.path[].sortOrder |
Integer | 同级排序号 |
data.path[].rowVersion |
Integer | 当前 CAS 版本 |
请求示例
GET /admin/region/2090000000000000376/path HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <管理员令牌>
Accept: application/json
响应示例
{
"code": 200,
"message": "成功",
"data": {
"selectedId": "2090000000000000376",
"provinceId": "2090000000000000001",
"prefectureId": "2090000000000000035",
"countyId": "2090000000000000376",
"path": [
{
"id": "2090000000000000001",
"parentId": null,
"codeStandard": "GB/T 2260",
"regionCode": "110000",
"levelCode": "PROVINCE",
"depth": 1,
"name": "北京市",
"shortName": null,
"nodeKind": "ADMINISTRATIVE",
"navigable": true,
"selectable": true,
"hasChildren": true,
"currentlyEffective": true,
"status": "ACTIVE",
"validFrom": "2025-12-31",
"validTo": null,
"sortOrder": 100,
"rowVersion": 0
},
{
"id": "2090000000000000035",
"parentId": "2090000000000000001",
"codeStandard": "GB/T 2260",
"regionCode": "110100",
"levelCode": "PREFECTURE",
"depth": 2,
"name": "北京市",
"shortName": null,
"nodeKind": "ADMINISTRATIVE",
"navigable": true,
"selectable": true,
"hasChildren": true,
"currentlyEffective": true,
"status": "ACTIVE",
"validFrom": "2025-12-31",
"validTo": null,
"sortOrder": 100,
"rowVersion": 0
},
{
"id": "2090000000000000376",
"parentId": "2090000000000000035",
"codeStandard": "GB/T 2260",
"regionCode": "110101",
"levelCode": "COUNTY",
"depth": 3,
"name": "东城区",
"shortName": null,
"nodeKind": "ADMINISTRATIVE",
"navigable": true,
"selectable": true,
"hasChildren": false,
"currentlyEffective": true,
"status": "ACTIVE",
"validFrom": "2025-12-31",
"validTo": null,
"sortOrder": 100,
"rowVersion": 0
}
]
},
"success": true
}
空数据 / 降级响应
本接口不返回“成功但空父链”。目标不存在、父链不完整或超过安全深度时使用错误响应,不返回部分路径。
{
"code": 210801,
"message": "行政区划节点不存在",
"data": null,
"success": false
}
错误响应
父链存在孤儿、越级、循环或异常终止:
{
"code": 210811,
"message": "行政区划父链数据不完整",
"data": null,
"success": false
}
业务边界
path必须从省级根开始,parentId、depth和levelCode逐级连续,最后一个节点必须等于selectedId。- 目标为省级时
prefectureId、countyId均为null;目标为地级时只有countyId=null,绝不会返回字符串"null"。 - 历史停用节点允许回显,节点自身
currentlyEffective=false;前端只能展示历史值,不能把它重新加入当前可选列表。 - 父链异常时整体失败,不返回可被误用的部分路径。
- 未登录返回业务码
401;不存在节点返回210801。
3. 创建行政区划节点 POST /admin/region
VO: AdministrativeRegionCreateReqVO / String
使用场景
具有 system:region:create 权限的管理员创建自定义业务行政区,或创建只用于层级导航、不可作为最终业务值的聚合节点。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
Authorization |
Header | String | ✅ | Bearer <token> 且具有 system:region:create |
管理端登录令牌 |
codeStandard |
Body | String | ✅ | 1~32 字符;聚合节点必须为 HL_GROUP |
编码命名空间,自定义行政区推荐 HL_CUSTOM |
regionCode |
Body | String | ✅ | 1~64 字符;首字符为字母或数字;仅含字母、数字、.、_、:、- |
编码标准内的唯一地区代码;服务端转大写 |
parentId |
Body | String / null | ❌ | 十进制正整数;创建省级根节点时为 null |
父节点 ID |
levelCode |
Body | String | ✅ | 1~32 字符;省/市/县必须匹配派生深度 | PROVINCE、PREFECTURE、COUNTY |
nodeKind |
Body | String | ✅ | ADMINISTRATIVE 或 AGGREGATION |
节点业务类型 |
name |
Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
shortName |
Body | String / null | ❌ | 最多 64 字符;空白归一为 null |
简称 |
navigable |
Body | Boolean | ✅ | 聚合节点固定为 true |
是否允许继续向下导航 |
selectable |
Body | Boolean | ✅ | 聚合节点固定为 false |
是否允许作为最终业务值 |
validFrom |
Body | String | ✅ | yyyy-MM-dd |
生效日期 |
validTo |
Body | String / null | ❌ | yyyy-MM-dd,必须严格晚于 validFrom |
失效日期;null 表示持续有效 |
status |
Body | String | ✅ | ACTIVE 或 INACTIVE |
节点状态 |
sortOrder |
Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
sourceVersion |
Body | String | ✅ | 非空,最多 64 字符 | 可审计的数据来源版本 |
sourceRef |
Body | String | ✅ | 非空,最多 512 字符 | 来源 URL、文件定位或管理批次标识 |
extension |
Body | Object / null | ❌ | 必须是 JSON object,序列化后最多 4000 字符 | 结构化扩展;旧入参别名 extJson 仍兼容 |
depth、操作人和来源校验值由服务端派生,禁止由前端提交。
出参 Result<String>
| 字段 | 类型 | 说明 |
|---|---|---|
code |
Integer | 200 表示创建成功 |
message |
String | 响应说明 |
success |
Boolean | 创建成功为 true |
data |
String | 新节点 ID,始终为 JSON string |
请求示例
POST /admin/region HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <具有 system:region:create 的管理员令牌>
Content-Type: application/json
{
"codeStandard": "HL_CUSTOM",
"regionCode": "DEMO-COUNTY-001",
"parentId": "2090000000000000035",
"levelCode": "COUNTY",
"nodeKind": "ADMINISTRATIVE",
"name": "示例业务区",
"shortName": "示例区",
"navigable": true,
"selectable": true,
"validFrom": "2026-08-26",
"validTo": null,
"status": "ACTIVE",
"sortOrder": 10000,
"sourceVersion": "ADMIN-20260826",
"sourceRef": "工单或数据批次定位",
"extension": {
"businessNote": "示例扩展信息"
}
}
响应示例
{
"code": 200,
"message": "成功",
"data": "2090000000000001001",
"success": true
}
空数据 / 降级响应
本接口成功时一定返回字符串 ID,不存在 data=null 的成功语义。失败时不创建节点。
错误响应
层级代码与父链派生深度不一致:
{
"code": 210813,
"message": "行政区划层级代码与父链深度不一致",
"data": null,
"success": false
}
业务边界
- 当前最大写入深度为 3;省、市、县层级必须分别使用
PROVINCE、PREFECTURE、COUNTY。 - 父节点必须存在、可导航、有效期完整覆盖子节点,并且不会让写入深度超过三级。
AGGREGATION必须同时满足codeStandard=HL_GROUP、navigable=true、selectable=false;它只能导航,不能作为最终业务值。ADMINISTRATIVE不得占用HL_GROUP;旧命名空间HL_INTERNAL不允许产生新写入。- 同一
codeStandard + regionCode的版本有效期不得重叠;有效期采用左闭右开[validFrom, validTo)。 - 缺少权限返回
210802;空请求或格式错误返回400;全部失败路径零业务写入。
4. 更新行政区划节点 PUT /admin/region/{id}
VO: AdministrativeRegionUpdateReqVO / Void
使用场景
具有 system:region:update 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带最近读取到的完整可变字段和 rowVersion。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
Authorization |
Header | String | ✅ | Bearer <token> 且具有 system:region:update |
管理端登录令牌 |
id |
Path | String | ✅ | 十进制正整数 | 要更新的节点 ID |
parentId |
Body | String / null | ❌ | 十进制正整数;省级根节点为 null |
更新后的父节点 ID |
levelCode |
Body | String | ✅ | 1~32 字符;必须匹配更新后父链深度 | 省/市/县层级代码 |
name |
Body | String | ✅ | 非空,最多 128 字符 | 完整名称 |
shortName |
Body | String / null | ❌ | 最多 64 字符;空白归一为 null |
简称 |
navigable |
Body | Boolean | ✅ | 聚合节点必须保持 true |
是否允许继续向下导航 |
selectable |
Body | Boolean | ✅ | 聚合节点必须保持 false |
是否允许作为最终业务值 |
validFrom |
Body | String | ✅ | yyyy-MM-dd |
生效日期 |
validTo |
Body | String / null | ❌ | 必须严格晚于 validFrom |
失效日期 |
status |
Body | String | ✅ | ACTIVE 或 INACTIVE |
节点状态 |
sortOrder |
Body | Integer | ✅ | 0~2,000,000,000 | 同级排序号 |
extension |
Body | Object / null | ❌ | 必须是 JSON object,最多 4000 字符 | 结构化扩展;旧别名 extJson 仍兼容 |
rowVersion |
Body | Integer | ✅ | 大于等于 0,必须等于当前版本 | CAS 并发版本,成功后自动加 1 |
codeStandard、regionCode、nodeKind、sourceVersion 和 sourceRef 不属于更新接口,不能通过请求改写。
出参 Result<Void>
| 字段 | 类型 | 说明 |
|---|---|---|
code |
Integer | 200 表示更新成功 |
message |
String | 成功时为“行政区划更新成功” |
success |
Boolean | 更新成功为 true |
data |
null | 更新接口不返回业务数据 |
请求示例
PUT /admin/region/2090000000000001001 HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <具有 system:region:update 的管理员令牌>
Content-Type: application/json
{
"parentId": "2090000000000000035",
"levelCode": "COUNTY",
"name": "示例业务区(更新)",
"shortName": "示例区",
"navigable": true,
"selectable": true,
"validFrom": "2026-08-26",
"validTo": null,
"status": "ACTIVE",
"sortOrder": 10001,
"extension": {
"businessNote": "名称已更新"
},
"rowVersion": 0
}
响应示例
{
"code": 200,
"message": "行政区划更新成功",
"data": null,
"success": true
}
空数据 / 降级响应
本接口成功时 data=null 是固定契约,调用方以 code=200 && success=true 判断成功。
错误响应
提交了已经过期的 rowVersion:
{
"code": 210807,
"message": "行政区划已被其他操作更新,请刷新后重试",
"data": null,
"success": false
}
业务边界
- 更新采用完整可变字段快照,不是局部 PATCH;前端必须先读取最新节点信息再提交。
rowVersion冲突返回210807且零写入;前端应重新加载,而不是自动覆盖重试。- 有子节点的分支禁止移动父级或改变层级,返回
210810。 - 父节点提前停用、取消导航或缩短有效期,导致现有子节点失去合法父级时返回
210814。 - 已到生效日的
GB/T 2260法定节点不能原位改写历史属性;应先关闭旧版本,再创建有效期不重叠的新版本。 - 成功后
rowVersion增加 1;再次编辑必须使用新版本号。
四、契约约束与正确调用方式
三级联动新建流程
GET /admin/region/children
→ 用户选择 provinceId
GET /admin/region/children?parentId={provinceId}
→ 用户选择 prefectureId
GET /admin/region/children?parentId={prefectureId}
→ 用户选择 countyId
→ 业务表单最终保存 countyId(或业务允许的上级 selectable 节点)
编辑历史值回显流程
GET /admin/region/{savedRegionId}/path
→ 读取 provinceId / prefectureId / countyId
→ 按 path 顺序逐层调用 children
→ 每层把对应历史 ID 作为 selectedId
→ 历史节点 currentlyEffective=false 时只展示历史值,不重新作为当前选项提交
✅ 正确 / ❌ 错误调用对照
| 场景 | 调用或 payload |
|---|---|
| ✅ 加载省级 | GET /admin/region/children,省略 parentId |
| ✅ 加载市级 | GET /admin/region/children?parentId={provinceId} |
| ✅ 编辑回显 | 先 GET /admin/region/{savedId}/path,再逐层加载 children |
| ✅ 比较 ID | 全程以 string 比较,例如 item.id === selectedId |
| ❌ 根层传 0 | GET /admin/region/children?parentId=0 → 400 |
| ❌ 截行政区编码推父级 | 根据 110101 截取 110100;父子关系只能以接口返回的 ID 为准 |
| ❌ 把聚合节点当最终值 | nodeKind=AGGREGATION && selectable=false 时仍提交为业务行政区 |
| ❌ 强转 number | Number("2090000000000000376") 会丢失精度 |
统一响应判断
if (response.code !== 200 || response.success !== true) {
throw new Error(response.message || '行政区划接口调用失败');
}
禁止只判断 HTTP status;业务错误可能使用 HTTP 200 返回。
五、数据库行为
| 前端操作 | 外部可观察的持久化行为 | 失败行为 |
|---|---|---|
查询 children |
只读,不改变节点、版本或审计 | 不写入 |
查询 path |
只读,不改变节点、版本或审计 | 不写入,不返回部分父链 |
| 创建节点 | 成功新增一个节点,初始 rowVersion=0;事务提交后联动和父链读取可见 |
参数、权限、父链、有效期或唯一性失败时不新增节点 |
| 更新节点 | 成功更新一个节点并令 rowVersion+1;事务提交后联动和父链返回新值 |
版本冲突或业务 guard 失败时原节点保持不变 |
前端无需理解或操作物理表、缓存键和审计表,也不得通过其他接口绕过创建/更新权限和版本检查。
六、边界行为
- 未登录:业务码
401。 - 已登录但缺少创建/更新专用权限:
210802。 - Query/Path ID 为 0、负数或非十进制正整数:
400。 - 父节点或目标节点不存在:
210801。 - 父级不可导航、有效期不能覆盖子级:
210803。 - 写入超过当前三级边界:
210804。 - 自引用或父链成环:
210805。 - 当前编码或版本冲突:
210806/210815。 rowVersion过期:210807,零写入。validTo不晚于validFrom:210808。- 父链异常:
210811,不返回部分路径。 extension不是 JSON object 或过长:210812。- 层级代码与父链深度不一致:
210813。 - 老数据兼容:停用历史节点可由
path回显;旧缓存中的数字 ID 可由后端读取,但当前 API 响应始终输出 string ID。
业务错误码
| code | message | 典型接口与场景 |
|---|---|---|
400 |
参数校验失败文案 | 任一接口的 ID 非正整数、Body 缺少必填字段或格式不合法 |
401 |
未认证文案 | 任一接口未携带有效管理端身份 |
210801 |
行政区划节点不存在 | children 父节点或 path/update 目标不存在 |
210802 |
无行政区划维护权限 | create / update 缺少专用写权限 |
210803 |
行政区划父节点不合法 | 父级不可导航、已失效或有效期不能覆盖子级 |
210804 |
行政区划层级超过当前允许深度 | create / update 试图写入第四级 |
210805 |
行政区划父链存在循环 | update 自引用或移动后形成祖先环 |
210806 |
行政区划当前编码已存在 | create 唯一编码冲突 |
210807 |
行政区划已被其他操作更新,请刷新后重试 | update.rowVersion 已过期 |
210808 |
行政区划有效期不合法 | validTo 不晚于 validFrom |
210809 |
该行政区划编码标准为系统保留值 | 新写入继续使用旧 HL_INTERNAL |
210810 |
存在子节点的行政区划不能移动或变更层级 | update 移动分支或改变分支层级 |
210811 |
行政区划父链数据不完整 | path 遇到孤儿、越级、循环或异常终止 |
210812 |
行政区划扩展字段必须是合法 JSON | extension 不是 JSON object 或内容过长 |
210813 |
行政区划层级代码与父链深度不一致 | 省/市/县代码与派生深度不匹配 |
210814 |
行政区划更新与现有子节点状态或有效期冲突 | 父级提前停用、取消导航或缩短有效期 |
210815 |
行政区划编码的版本有效期与既有数据重叠 | 同编码的两个版本时间窗重叠 |
210816 |
行政区划节点类型或编码命名空间不合法 | nodeKind、codeStandard、可导航/可选组合错误 |
210817 |
已生效法定行政区划只能关闭旧版本后新增 | 原位改写生效中的 GB/T 2260 节点 |
210818 |
行政区划数据来源信息不完整 | sourceVersion 或 sourceRef 缺失/过长 |
210819 |
法定行政区划代码必须为六位数字 | GB/T 2260 的 regionCode 格式错误 |
六.5、枚举 / 数据字典
levelCode(行政区划层级)
所属字段: AdministrativeRegionCreateReqVO.levelCode、AdministrativeRegionUpdateReqVO.levelCode、AdministrativeRegionItemVO.levelCode | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
PROVINCE |
省级 | 深度 1,父节点必须为 null |
PREFECTURE |
地级 | 深度 2,父节点必须是省级 |
COUNTY |
县级 | 深度 3,父节点必须是地级 |
nodeKind(节点类型)
所属字段: AdministrativeRegionCreateReqVO.nodeKind、AdministrativeRegionItemVO.nodeKind | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
ADMINISTRATIVE |
行政区节点 | 可按 selectable 决定是否作为最终业务值;不得使用 HL_GROUP |
AGGREGATION |
导航聚合节点 | 必须使用 HL_GROUP、navigable=true、selectable=false |
status(节点状态)
所属字段: 创建/更新请求及节点响应的 status | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
ACTIVE |
启用 | 位于有效期时可进入当前联动列表 |
INACTIVE |
停用 | 不进入当前联动列表,但历史父链仍可回显 |
codeStandard(编码命名空间)
所属字段: AdministrativeRegionCreateReqVO.codeStandard、AdministrativeRegionItemVO.codeStandard | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
GB/T 2260 |
法定行政区编码 | regionCode 必须为六位数字;生效历史受不可变保护 |
HL_CUSTOM |
自定义行政区编码 | 管理端创建普通自定义行政区的推荐命名空间 |
HL_GROUP |
导航聚合编码 | 只能与 AGGREGATION 搭配 |
六.6、修改前后对比
本条为新增接口,没有需要兼容的旧 /admin/region/** 公开接口。补充工单 #6409 在正式前端接入前固定了 ID 输出类型:
字段级对比
| 字段 | 初始实现风险 | 当前已部署契约 |
|---|---|---|
| 所有行政区划 Long ID | 小数值可能被全局序列化规则输出为 JSON number | 一律输出 JSON string |
| 缺失父级/层级 | 可能被调用方误当字符串处理 | 保持真正的 JSON null |
extension 历史入参 |
旧调用方可能使用 extJson |
请求继续兼容 extJson,当前文档统一使用 extension |
行为级对比
| 行为 | 接入前 | 当前已部署行为 |
|---|---|---|
| 三级联动 | 无统一公开接口,容易截行政区编码推断 | 逐级调用 children,以后端父子关系为准 |
| 编辑回显 | 调用方自行拼装父链 | 调用 path 返回完整有序父链和三级快捷 ID |
| 并发更新 | 无本接口旧语义 | 必须提交 rowVersion,冲突返回 210807 且零写入 |
六.7、影响评估
- 是否破坏向后兼容: 否;这是新增接口,且正式前端接入前已经固定字符串 ID 契约。
- 前端是否必须同步上线: 是;
frontend_status=pending,管理端需接入三级选择器、历史回显和主数据维护。 - 前端 workaround 清理点: 不得保留行政区编码截位、Long ID 转 number、只判断 HTTP status 或把
hasChildren等同于selectable的临时逻辑。
七、不影响范围
- 仅影响: 管理后台行政区划维护、省/市/县三级选择器及历史行政区值回显。
- 零影响:
- 小程序端现有行政区接口;
- Product、Order、Resource 和 Fleet 现有业务接口;
- 已保存业务数据中的行政区 ID;
- 登录、权限菜单和非行政区划缓存;
- 生产环境(本 Changelog 只证明 TEST 已部署和验收)。
八、测试环境已验证
GET /admin/region/children → 根级/子级列表、defaultId、string/null ID ✓
GET /admin/region/{id}/path → 省市县完整父链、缺失层级 null、string ID ✓
POST /admin/region → 创建成功返回 string ID,数据库与缓存读取一致 ✓
PUT /admin/region/{id} → CAS 更新成功,rowVersion 0→1,联动与父链同步 ✓
GET /admin/region/children(未认证) → 401 ✓
POST /admin/region(缺参/非法节点类型) → 400 且零业务写入 ✓
GET /admin/region/{missingId}/path → 210801 ✓
- TEST 精确提交:
b62054035a139e3c689179ef75e2d052d08dde52,包含六个相关 PR 的合并提交。 - Deploy Panel API 任务:
52771074(hl-user-service)和b1db24b2(hl-gateway),终态均为success,双实例、Nacos、滚动采样及部署窗口日志检查通过。 - 真实 TEST 管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、持久化结果和缓存一致性验收。
- 验收创建的临时节点已精确清理;数据库数量与测试命名空间恢复基线,四个缓存快照按原值和绝对过期时间恢复,登录会话已注销;操作审计按系统设计保留。
九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|---|---|---|---|
| #6359 | #6350 | 行政区划主数据、三级写入与读接口基础实现 | ✅ 有效 |
| #6387 | #6384 | 权限、来源和节点类型契约补强 | ✅ 有效 |
| #6393 | #6389 | 有效期、父链和历史版本保护补强 | ✅ 有效 |
| #6398 | #6395 | Gateway 路由与认证策略补齐 | ✅ 有效 |
| #6402 | #6399 | 缓存一致性与滚动兼容补强 | ✅ 有效 |
| #6412 | #6409 | 所有行政区划响应 ID 固定为 JSON string | ✅ 有效 |
十、相关文档
- 主 Issue: wx/HL#6350
- ID 字符串补充 Issue: wx/HL#6409
- 主 PR: wx/HL#6359
- ID 字符串 PR: wx/HL#6412
- 本次文档补充 Issue: wx/HL#6422
撤回
本次 #6422 仅补充 Changelog 和模板校验,不改变后端制品。若文档补充需要撤回,从最新 hl-api-changelog/main 创建独立分支,revert #6422 对应合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据、配置、Redis 和 MQ 均不需要回退。撤回后前端暂以当前 Controller/VO 契约为准,并重新提交一份通过模板门禁的自包含文档。
关联 / 联系人
链接
联系人
- 后端负责人: @lc