--- schema: "hl-changelog/v2" ticket: "6350" title: "行政区划主数据与三级联动" consumer: "admin" author: "lc(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "主工单 #6350 及补充工单 #6384、#6389、#6395、#6399、#6409 的六个 PR 均已合并 dev-v3。精确提交 b62054035a139e3c689179ef75e2d052d08dde52 已部署 TEST;真实管理员经 Gateway 完成未认证、读取、失败零写入、创建、CAS 更新、数据库和 Redis 一致性验收。#6422 已按 CHANGELOG_TEMPLATE.md 补齐四个接口的前端联调契约。" updated_at: "2026-08-26" base: "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 ` | 真实管理端登录令牌 | | `parentId` | Query | String | ❌ | 十进制正整数;根层省级列表必须省略 | 指定要查询直接子节点的父节点 ID | | `selectedId` | Query | String | ❌ | 十进制正整数 | 编辑回显时希望优先选中的本级节点 ID | 无请求体。 #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `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 版本,更新时原样提交 | #### 请求示例 ```http GET /admin/region/children?parentId=2090000000000000001&selectedId=2090000000000000035 HTTP/1.1 Host: api.test.1814.love:9443 Authorization: Bearer <管理员令牌> Accept: application/json ``` 根层省级列表: ```http GET /admin/region/children HTTP/1.1 Host: api.test.1814.love:9443 Authorization: Bearer <管理员令牌> Accept: application/json ``` #### 响应示例 ```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 } ``` #### 空数据 / 降级响应 父节点存在但没有当前有效直接子节点时,返回成功空数组: ```json { "code": 200, "message": "成功", "data": { "parentId": "2090000000000000035", "items": [], "defaultId": null }, "success": true } ``` #### 错误响应 父节点不存在或已删除: ```json { "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 ` | 真实管理端登录令牌 | | `id` | Path | String | ✅ | 十进制正整数 | 要回显的省、市或县节点 ID | 无请求体。 #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `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 版本 | #### 请求示例 ```http GET /admin/region/2090000000000000376/path HTTP/1.1 Host: api.test.1814.love:9443 Authorization: Bearer <管理员令牌> Accept: application/json ``` #### 响应示例 ```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 } ``` #### 空数据 / 降级响应 本接口不返回“成功但空父链”。目标不存在、父链不完整或超过安全深度时使用错误响应,不返回部分路径。 ```json { "code": 210801, "message": "行政区划节点不存在", "data": null, "success": false } ``` #### 错误响应 父链存在孤儿、越级、循环或异常终止: ```json { "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 ` 且具有 `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` | 字段 | 类型 | 说明 | |---|---|---| | `code` | Integer | `200` 表示创建成功 | | `message` | String | 响应说明 | | `success` | Boolean | 创建成功为 `true` | | `data` | String | 新节点 ID,始终为 JSON string | #### 请求示例 ```http 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": "示例扩展信息" } } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": "2090000000000001001", "success": true } ``` #### 空数据 / 降级响应 本接口成功时一定返回字符串 ID,不存在 `data=null` 的成功语义。失败时不创建节点。 #### 错误响应 层级代码与父链派生深度不一致: ```json { "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 ` 且具有 `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` | 字段 | 类型 | 说明 | |---|---|---| | `code` | Integer | `200` 表示更新成功 | | `message` | String | 成功时为“行政区划更新成功” | | `success` | Boolean | 更新成功为 `true` | | `data` | null | 更新接口不返回业务数据 | #### 请求示例 ```http 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 } ``` #### 响应示例 ```json { "code": 200, "message": "行政区划更新成功", "data": null, "success": true } ``` #### 空数据 / 降级响应 本接口成功时 `data=null` 是固定契约,调用方以 `code=200 && success=true` 判断成功。 #### 错误响应 提交了已经过期的 `rowVersion`: ```json { "code": 210807, "message": "行政区划已被其他操作更新,请刷新后重试", "data": null, "success": false } ``` #### 业务边界 - 更新采用完整可变字段快照,不是局部 PATCH;前端必须先读取最新节点信息再提交。 - `rowVersion` 冲突返回 `210807` 且零写入;前端应重新加载,而不是自动覆盖重试。 - 有子节点的分支禁止移动父级或改变层级,返回 `210810`。 - 父节点提前停用、取消导航或缩短有效期,导致现有子节点失去合法父级时返回 `210814`。 - 已到生效日的 `GB/T 2260` 法定节点不能原位改写历史属性;应先关闭旧版本,再创建有效期不重叠的新版本。 - 成功后 `rowVersion` 增加 1;再次编辑必须使用新版本号。 --- ## 四、契约约束与正确调用方式 ### 三级联动新建流程 ```text GET /admin/region/children → 用户选择 provinceId GET /admin/region/children?parentId={provinceId} → 用户选择 prefectureId GET /admin/region/children?parentId={prefectureId} → 用户选择 countyId → 业务表单最终保存 countyId(或业务允许的上级 selectable 节点) ``` ### 编辑历史值回显流程 ```text 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")` 会丢失精度 | ### 统一响应判断 ```javascript 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 已部署和验收)。 --- ## 八、测试环境已验证 ```text 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](https://git.1814.love:8443/wx/HL/issues/6350) - ID 字符串补充 Issue: [wx/HL#6409](https://git.1814.love:8443/wx/HL/issues/6409) - 主 PR: [wx/HL#6359](https://git.1814.love:8443/wx/HL/pulls/6359) - ID 字符串 PR: [wx/HL#6412](https://git.1814.love:8443/wx/HL/pulls/6412) - 本次文档补充 Issue: [wx/HL#6422](https://git.1814.love:8443/wx/HL/issues/6422) --- ## 撤回 本次 #6422 仅补充 Changelog 和模板校验,不改变后端制品。若文档补充需要撤回,从最新 `hl-api-changelog/main` 创建独立分支,revert #6422 对应合并提交并重新运行仓库全部校验;TEST 行政区划 API、数据、配置、Redis 和 MQ 均不需要回退。撤回后前端暂以当前 Controller/VO 契约为准,并重新提交一份通过模板门禁的自包含文档。 --- ## 关联 / 联系人 ### 链接 - **Issue**: [#6350](https://git.1814.love:8443/wx/HL/issues/6350) - **补充文档 Issue**: [#6422](https://git.1814.love:8443/wx/HL/issues/6422) - **PR**: [#6359](https://git.1814.love:8443/wx/HL/pulls/6359) - **ID 契约 PR**: [#6412](https://git.1814.love:8443/wx/HL/pulls/6412) - **最终后端提交**: [b62054035](https://git.1814.love:8443/wx/HL/commit/b62054035a139e3c689179ef75e2d052d08dde52) ### 联系人 - **后端负责人**: @lc