文件
hl-api-changelog/changelogs-v2/2026-08/26_6350_行政区划主数据与三级联动-新增接口-管理后台.md
lc 3a2c1ae38d
changelog-filename-gate / validate (pull_request) Successful in 2s
fix(changelog): 按 TEST 代码修正行政区划契约 (#6438)
2026-08-26 17:59:58 +08:00

40 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 verified mmg 7acd1cb0 v2.1 2026-08-26 主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约,三级联动由 mmg 以 ref 7acd1cb0 完成验证;#6438 再按 TEST 代码修正种子父链、聚合节点、可扩展编码命名空间、异步缓存刷新和 extension 更新限制。 2026-08-26 dev-v3

User: 行政区划主数据与三级联动

服务: hl-user-service(统一经 Gateway 访问)

PR: #6359、#6387、#6393、#6398、#6402、#6412

Issue: #6350、#6384、#6389、#6395、#6399、#6409、#6422、#6438

日期: 2026-08-26

影响范围: 管理后台行政区划维护、省/市/县三级选择器和历史值父链回显


⚠️ 关键变化

  • 本次补文档不改变 TEST 上已经部署的接口行为;它把原 Changelog 中分散的说明整理为可直接联调的完整契约。
  • 前端不得再把行政区划 ID 当 JavaScript number:响应中的所有行政区划 ID 均为 JSON string,缺失层级保持真正的 null。
  • 三级联动不允许通过行政区编码截位推断父子关系;必须逐级调用 children,编辑回显先调用 path。
  • PREFECTURE 是第二层导航层,不保证一定是可选择的法定地级行政区;直辖市等链路会返回 AGGREGATION 聚合节点,必须继续下钻且不得作为最终业务值。
  • 当前没有单节点详情接口,读响应也不返回 extension;更新接口省略或传 null 都会清空该字段,前端不能把它理解成“保持原值”。
  • 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 code、success、message 和 data。

一、背景

管理后台需要一套统一的行政区划主数据能力,同时支持:

  • 省、市、县三级选择器逐级加载;
  • 编辑历史业务数据时,从任意节点恢复完整父链;
  • 具有专用权限的管理员创建自定义行政区、带来源证据的新法定版本或导航聚合节点;
  • 使用 rowVersion 防止两位管理员并发更新时互相覆盖。

接口当前只开放省、市、县三级写入,层级代码依次为 PROVINCE、PREFECTURE、COUNTY。第二层可能是 AGGREGATION 导航分组,不等同于可提交的法定地级行政区。下文读取示例使用 TEST migration 中的北京链路 1 → 35 → 376,仅用于说明响应结构,业务代码仍不得硬编码。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
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=1&selectedId=35 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": "1",
    "items": [
      {
        "id": "35",
        "parentId": "1",
        "codeStandard": "HL_GROUP",
        "regionCode": "HL-GROUP-110000",
        "levelCode": "PREFECTURE",
        "depth": 2,
        "name": "北京市辖区",
        "shortName": null,
        "nodeKind": "AGGREGATION",
        "navigable": true,
        "selectable": false,
        "hasChildren": true,
        "currentlyEffective": true,
        "status": "ACTIVE",
        "validFrom": "2025-12-31",
        "validTo": null,
        "sortOrder": 2000000001,
        "rowVersion": 0
      }
    ],
    "defaultId": "35"
  },
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}

空数据 / 降级响应

父节点存在但没有当前有效直接子节点时,返回成功空数组。Redis 读取失败时服务会降级查数据库,成功结构不变:

{
  "code": 200,
  "message": "成功",
  "data": {
    "parentId": "376",
    "items": [],
    "defaultId": null
  },
  "success": true
}

错误响应

父节点不存在或已删除:

{
  "code": 210801,
  "message": "行政区划节点不存在",
  "data": null,
  "success": false
}

业务边界

  • 只返回 status=ACTIVE 且当前有效的直接子节点,停用或历史节点不会混入当前选择项。
  • selectedId 不在本次 items 中时不会报错,而是稳定回退到排序后的第一项;空列表回退为 null。
  • 排序固定为 sortOrder、regionCode、id,前端不得另用行政区编码推断顺序或父子关系。
  • navigable 控制能否继续向下加载,selectable 控制能否作为最终值;两者不能相互替代。
  • defaultId 只表示本级默认导航项,不保证 selectable=true;例如北京第二层默认项 "35" 是不可提交的聚合节点。
  • 未登录返回业务码 401;parentId=0 等非正整数参数返回业务码 400。

2. 父链回显 GET /admin/region/{id}/path

VO: AdministrativeRegionPathVO / AdministrativeRegionItemVO

使用场景

编辑已有业务数据时,后端只保存了一个最终行政区 ID。前端先调用本接口得到完整三级父链,再按父链逐层调用 children 加载每一级导航项。父链的第二层可能是不可选择的 AGGREGATION,不能仅凭 prefectureId 字段名判断它一定是法定地级行政区。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ Bearer <token> 真实管理端登录令牌
id Path String ✅ 十进制正整数 要回显的省、市或县节点 ID

无请求体。

出参 Result<AdministrativeRegionPathVO>

字段 类型 说明
code Integer 200 表示成功;业务失败读取具体业务码
message String 响应说明
success Boolean 仅当 code=200 时为 true
traceId String / null 链路追踪 ID;报错排查时提供给后端
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/376/path HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <管理员令牌>
Accept: application/json

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "selectedId": "376",
    "provinceId": "1",
    "prefectureId": "35",
    "countyId": "376",
    "path": [
      {
        "id": "1",
        "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": 10,
        "rowVersion": 0
      },
      {
        "id": "35",
        "parentId": "1",
        "codeStandard": "HL_GROUP",
        "regionCode": "HL-GROUP-110000",
        "levelCode": "PREFECTURE",
        "depth": 2,
        "name": "北京市辖区",
        "shortName": null,
        "nodeKind": "AGGREGATION",
        "navigable": true,
        "selectable": false,
        "hasChildren": true,
        "currentlyEffective": true,
        "status": "ACTIVE",
        "validFrom": "2025-12-31",
        "validTo": null,
        "sortOrder": 2000000001,
        "rowVersion": 0
      },
      {
        "id": "376",
        "parentId": "35",
        "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": 20,
        "rowVersion": 0
      }
    ]
  },
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}

空数据 / 降级响应

本接口不返回“成功但空父链”。目标不存在、父链不完整或超过当前三级安全边界时使用错误响应,不返回部分路径。Redis 不可用时整条父链降级为同一数据库事务快照读取,不拼接缓存与数据库的半条路径。

{
  "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"。
  • prefectureId 只是路径第二层快捷 ID;直辖市等链路可指向 nodeKind=AGGREGATION && selectable=false 的聚合节点。
  • 历史停用或过期节点允许回显,节点自身 currentlyEffective=false,且后端会把该节点的 navigable、selectable 强制返回 false;前端只能展示历史值,不能把它重新加入当前可选列表。
  • 父链异常时整体失败,不返回可被误用的部分路径。
  • 未登录返回业务码 401;不存在节点返回 210801。

3. 创建行政区划节点 POST /admin/region

VO: AdministrativeRegionCreateReqVO / String

使用场景

具有 system:region:create 权限的管理员可以创建自定义业务行政区、带完整来源证据且不与旧版本重叠的 GB/T 2260 新版本,或只用于层级导航、不可作为最终业务值的聚合节点。

入参

字段 位置 类型 必填 约束 说明
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
traceId String / null 链路追踪 ID;报错排查时提供给后端
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": "35",
  "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",
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}

空数据 / 降级响应

本接口成功时一定返回字符串 ID,不存在 data=null 的成功语义。失败时事务回滚且不创建节点;本接口没有“Redis 不可用仍受理写入”的公开降级承诺。

错误响应

层级代码与父链派生深度不一致:

{
  "code": 210813,
  "message": "行政区划层级代码与父链深度不一致",
  "data": null,
  "success": false
}

业务边界

  • 当前最大写入深度为 3;省、市、县层级必须分别使用 PROVINCE、PREFECTURE、COUNTY。
  • 父节点必须存在、可导航、有效期完整覆盖子节点,并且不会让写入深度超过三级。
  • AGGREGATION 必须同时满足 codeStandard=HL_GROUP、navigable=true、selectable=false;它只能导航,不能作为最终业务值。
  • ADMINISTRATIVE 不得占用 HL_GROUP;除 HL_GROUP、HL_INTERNAL 的限制外,codeStandard 不是封闭枚举,自定义命名空间可扩展,推荐使用 HL_CUSTOM。
  • 旧命名空间 HL_INTERNAL 不允许产生新写入;GB/T 2260 只允许六位数字 regionCode,并要求完整的 sourceVersion、sourceRef。
  • 同一 codeStandard + regionCode 的版本有效期不得重叠;有效期采用左闭右开 [validFrom, validTo)。
  • 缺少权限返回 210802;空请求或格式错误返回 400;全部失败路径零业务写入。

4. 更新行政区划节点 PUT /admin/region/{id}

VO: AdministrativeRegionUpdateReqVO / Void

使用场景

具有 system:region:update 权限的管理员更新节点名称、父级、层级、可导航/可选状态、有效期、排序和扩展信息。请求必须携带完整可变字段快照和最近读取到的 rowVersion;这不是局部 PATCH。

入参

字段 位置 类型 必填 约束 说明
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 字符;省略与显式 null 都会清空 结构化扩展;旧别名 extJson 仍兼容
rowVersion Body Integer ✅ 大于等于 0,必须等于当前版本 CAS 并发版本,成功后自动加 1

codeStandard、regionCode、nodeKind、sourceVersion 和 sourceRef 不属于更新接口,不能通过请求改写。

出参 Result<Void>

字段 类型 说明
code Integer 200 表示更新成功
message String 成功时为“行政区划更新成功”
success Boolean 更新成功为 true
traceId String / null 链路追踪 ID;报错排查时提供给后端
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": "35",
  "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,
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}

空数据 / 降级响应

本接口成功时 data=null 是固定契约,调用方以 code=200 && success=true 判断成功。本接口没有“Redis 不可用仍受理写入”的公开降级承诺,也不会返回更新后的节点快照。

错误响应

提交了已经过期的 rowVersion:

{
  "code": 210807,
  "message": "行政区划已被其他操作更新,请刷新后重试",
  "data": null,
  "success": false
}

业务边界

  • 更新采用完整可变字段快照,不是局部 PATCH;省略 extension 不表示保持原值,而是写成 null。
  • 当前 children / path 响应不包含 extension,也没有单节点详情接口。前端无法仅靠这四个公开接口保留未知的非空扩展值;在补充详情契约前,不应开放此类节点的通用编辑入口。
  • TEST 初始化聚合节点可返回大于 2,000,000,000 的 sortOrder(例如北京第二层为 2,000,000,001),而更新请求上限是 2,000,000,000;直接回填会返回 400,这类节点也不能按无损通用编辑处理。
  • rowVersion 冲突返回 210807 且零写入;前端应重新加载,而不是自动覆盖重试。
  • 有子节点的分支禁止移动父级或改变层级,返回 210810。
  • 父节点提前停用、取消导航或缩短有效期,导致现有子节点失去合法父级时返回 210814。
  • 已到生效日的 GB/T 2260 法定节点不能原位改写历史属性;代码只允许在其他历史字段(包含 extension)完全不变时关闭状态/结束时间。由于当前读接口不返回 extension,前端不能把“关闭已有法定版本”当成已具备的安全通用操作。
  • 成功后数据库中的 rowVersion 增加 1,但响应不返回新版本;再次编辑前需重新读取可见节点,且要考虑提交后缓存异步刷新的短暂窗口。

四、契约约束与正确调用方式

三级联动新建流程

GET /admin/region/children
  → 用户选择 provinceId
GET /admin/region/children?parentId={provinceId}
  → 用户选择第二层导航项 prefectureId(可能是不可提交的 AGGREGATION)
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
✅ 判断最终值 只提交 selectable=true 的节点;defaultId、prefectureId 本身不等于可提交
❌ 根层传 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 失败时原节点保持不变

写接口成功只证明数据库事务已经提交,不承诺紧随其后的第一个缓存读取已经反映新值。前端不得自行操作缓存;需要立即回显时应短暂重读,并始终以新的 rowVersion 为准。读取阶段 Redis 故障会降级数据库;写入口还受公共幂等保护,不能据此推导“Redis 故障时写入一定成功”。


六、边界行为

  • 未登录:业务码 401。
  • 已登录但缺少创建/更新专用权限:210802。
  • Query/Path ID 为 0、负数或非十进制正整数:400。
  • 父节点或目标节点不存在:210801。
  • 父级不可导航、有效期不能覆盖子级:210803。
  • 写入超过当前三级边界:210804。
  • 自引用或父链成环:210805。
  • 当前编码或版本冲突:210806 / 210815。
  • rowVersion 过期:210807,零写入。
  • validTo 不晚于 validFrom:210808。
  • 父链异常:210811,不返回部分路径。
  • extension 不是 JSON object 或过长:210812。
  • 层级代码与父链深度不一致:210813。
  • 写接口命中五秒参数幂等键:100502;这是拒绝重复提交,不会回放第一次成功响应。
  • 老数据兼容:停用历史节点可由 path 回显;旧缓存中的数字 ID 可由后端读取,但当前 API 响应始终输出 string ID。

业务错误码

code message 典型接口与场景
400 参数校验失败文案 任一接口的 ID 非正整数、Body 缺少必填字段或格式不合法
401 未认证文案 任一接口未携带有效管理端身份
100502 请勿重复提交 create / update 在五秒内命中相同参数幂等键;失败不会返回第一次结果
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

codeStandard 不是封闭枚举。服务端会去除首尾空白并转成大写;下表仅列出内置/保留语义:

值或类别 中文 说明
GB/T 2260 法定行政区编码 regionCode 必须为六位数字;生效历史受不可变保护
HL_CUSTOM 自定义行政区编码 管理端创建普通自定义行政区的推荐命名空间
HL_GROUP 导航聚合编码 只能与 AGGREGATION 搭配
HL_INTERNAL 旧内部命名空间 仅兼容识别旧缓存,新写入固定拒绝并返回 210809
其他非空值 扩展命名空间 最多 32 字符;可供 ADMINISTRATIVE 使用,但不得冒充 HL_GROUP

六.6、修改前后对比

本条为新增接口,没有需要兼容的旧 /admin/region/** 公开接口。补充工单 #6409 在正式前端接入前固定了 ID 输出类型:

字段级对比

字段 初始实现风险 当前已部署契约
所有行政区划 Long ID 小数值可能被全局序列化规则输出为 JSON number 一律输出 JSON string
缺失父级/层级 可能被调用方误当字符串处理 保持真正的 JSON null
extension 历史入参 旧调用方可能使用 extJson 请求继续兼容 extJson,当前文档统一使用 extension
TEST 北京第二层示例 曾被文档误写为可选择的 GB/T 2260 地级节点 实际为 id="35"、HL_GROUP、AGGREGATION、selectable=false 的导航分组

行为级对比

行为 接入前 当前已部署行为
三级联动 无统一公开接口,容易截行政区编码推断 逐级调用 children,以后端父子关系为准
编辑回显 调用方自行拼装父链 调用 path 返回完整有序父链和三级快捷 ID
并发更新 无本接口旧语义 必须提交 rowVersion,冲突返回 210807 且零写入
写后读取 文档曾表述为提交后立即可见 代码为事务提交后异步刷新缓存,存在短暂最终一致窗口

六.7、影响评估

  • 是否破坏向后兼容: 否;这是新增接口,且正式前端接入前已经固定字符串 ID 契约。
  • 前端接入状态: frontend_status=verified;mmg 已完成三级联动接入验证(frontend_ref=7acd1cb0)。该验证不扩大为通用主数据编辑能力,后者仍受 extension 不可回读等限制,不能按“完整 CRUD 已具备”上线。
  • 前端 workaround 清理点: 不得保留行政区编码截位、Long ID 转 number、只判断 HTTP status、把 hasChildren / defaultId / prefectureId 等同于 selectable,或把省略 extension 理解成保持原值的临时逻辑。

七、不影响范围

  • 仅影响: 管理后台行政区划维护、省/市/县三级选择器及历史行政区值回显。
  • 当前未提供: 单节点详情、历史版本列表、按编码查询、删除、批量导入/导出接口;四个已发布接口不能组成无损的完整 CRUD。
  • 前端接入边界: 三级选择与父链回显可直接接入;对已有非空 extension、超出更新上限的初始化 sortOrder、已生效法定节点关闭等需要无损回读/回填的场景暂不具备完整公开契约。
  • 零影响:
    • 小程序端现有行政区接口;
    • 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 的合并提交。
  • 本次代码优先复核确认:该提交的 AdminRegionController 只有本文四个公开端点;AdministrativeRegionItemVO 不含 extension;缓存刷新注册在事务 afterCommit 后交给独立执行器;migration 中北京链路为 1 → 35 → 376,其中 35 规范化后为不可选择的 HL_GROUP/AGGREGATION。
  • 2026-08-26 本次复核还通过 TEST Gateway 匿名请求确认 /admin/region/children 当前可达且返回业务码 401、success=false;未使用伪造身份,也未产生业务写入。
  • 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 ✅ 有效

十、相关文档


撤回

本次 #6438 代码优先核对只修改 Changelog,不改变后端制品。若本次文档修正需要撤回,从最新 hl-api-changelog/main 创建独立分支,revert 本次文档合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据库、配置、Redis 和 MQ 均不需要回退。撤回后前端必须以 TEST 对应提交的 Controller、VO、Service 与 migration 为准,不能恢复使用被本次指出的错误聚合节点示例或同步缓存假设。


关联 / 联系人

链接

联系人

  • 后端负责人: @lc