--- schema: "hl-changelog/v2" ticket: "7439" title: "团期车务地基(内部接口分册)——需求池 kind 过滤 / 变更历史按 kind 分组 / fleet 按 (orderId,kind) 拉取 / 接送机服务日回填" consumer: "internal" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "本文件是 #7439 的内部接口分册,与对外分册 13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md 同批交付。拆分依据 BACKEND_CHANGELOG_DELIVERY_GUIDE.md 2.5 节:/v3/internal/* Feign 接口必须单独成篇,不得与 admin/mp 接口塞同一份。" updated_at: "2026-09-13" base: "dev-v3" --- # order-v3: 团期车务地基(内部接口分册) **服务**: hl-order-service-v3 **消费方**: hl-fleet-service(Feign,`/v3/internal/**`,不经 JWT,靠网关不暴露该前缀做隔离) **PR**: #7551(`f035b85be`)、#7572(`600430303`)、#7595(`fdebc58c4`)、#7597(`bc75393fd`) **Issue**: #7439 > **本文件是分册,不是全部。** #7439 共改 11 个端点,其中 7 个对外端点(`/v3/admin/**`)在 > `13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md`。 > 两份合起来才是本单的完整契约面;**本册的收件人是后端与 fleet 侧,不是 mmg**。 --- ## ⚠️ 关键变化 1. **需求池按 kind 过滤**:`kind` 不传默认 TRAVEL,传 `TRANSFER` 取接送机池,传 **`ALL` 取两类** 2. **变更历史按 kind 分组**:不传 kind 时两类都返并**按 kind 分组**,不得混在一条时间线里 3. **按 (orderId, kind) 取当前生效需求**:同订单可并存 TRAVEL 与 TRANSFER 两条活跃需求,取数必须带 kind 4. **新增服务日回填端点**:补齐迁移遗留的空 `service_dates` 并推进需求状态,带 outcome 三态 **调用顺序是硬约束**(见「四、契约约束」):消费 TRANSFER 需求必须**先无条件调回填端点**、按 outcome 分流后 再调 `GET .../requirement/vehicle?kind=TRANSFER`;倒过来先 GET 判空再回填的写法永远走不到回填。 --- ## 一、背景 团期车务从单一用车需求(行程)扩展到双需求并存(+接送机)。 fleet 侧原先按 `orderId` 取唯一需求的假设在本单之后不再成立——**同一订单可以同时有两条活跃需求**, 所有内部读写口都必须显式带 `kind`,否则拿到的是哪一条取决于实现细节而不是契约。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 查询用车需求(内部) | GET | `/v3/internal/order/vehicle-requirements` | 改造 | 需求池按 kind 过滤,不传默认仅返 TRAVEL,传 ALL 取两类 | | 2 | 查询需求变更历史(内部) | GET | `/v3/internal/order/orders/{orderId}/requirement-history` | 改造 | 车需求历史按 kind 分组返回,不传 kind 时两类都返 | | 3 | 查询订单车需求(内部) | GET | `/v3/internal/order/orders/{orderId}/requirement/vehicle` | 改造 | fleet 按 (orderId, kind) 拉取当前生效需求 | | 4 | 回填接送机服务日(内部) | POST | `/v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill` | 新增 | 补齐迁移遗留的空服务日并推进需求状态 | --- ## 三、接口详情 ### 1. 查询用车需求(内部) `GET /v3/internal/order/vehicle-requirements` **VO**: `String status, String kind → Result>` #### 使用场景 fleet 拉取待配车需求池(调度用)。不经 JWT 鉴权,依赖网络隔离(网关不暴露 `/internal/**` 至公网)。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | status | Query | String | ❌ | 当前仅支持 PENDING | 需求状态过滤,默认 PENDING | | kind | Query | String | ❌ | TRAVEL/TRANSFER/ALL | 🆕 默认 TRAVEL;传 TRANSFER 取接送机池,传 ALL 取两类 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | requirementId/orderId/status/version/fleet/specialTags/pickupRequired/dropoffRequired/remark/dispatchRemark/submittedAt | - | 既有字段,结构不变 | | kind | String | 🆕 需求类别(TRAVEL/TRANSFER) | #### 请求示例 ```json GET /v3/internal/order/vehicle-requirements?status=PENDING&kind=TRANSFER ``` #### 响应示例 ```json { "code": 200, "data": [ { "requirementId": "90022334455", "orderId": "60123456789013", "status": "PENDING", "version": 1, "fleet": "[{\"carType\":\"商务车\",\"seats\":7,\"count\":1}]", "specialTags": ["中文司机", "大行李厢"], "pickupRequired": true, "dropoffRequired": true, "remark": "", "dispatchRemark": "", "submittedAt": "2026-09-10T14:35:10", "kind": "TRANSFER" } ], "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 200, "data": [], "success": true } ``` #### 错误响应 ```json { "code": 809000, "message": "用车需求类别非法", "success": false } ``` #### 业务边界 - 🔴 默认值是有意为之:不传 kind 时只返 TRAVEL 池,TRANSFER 不进现有池,避免 V-4 接线前 fleet 拉到服务日为 NULL 的行 - 传 kind=ALL 可取两类 - 不经 JWT,依赖网络隔离,网关不暴露 `/internal/**` 至公网 ### 2. 查询需求变更历史(内部) `GET /v3/internal/order/orders/{orderId}/requirement-history` **VO**: `Long orderId, String requirementType, String kind → Result` #### 使用场景 按订单查需求全量历史(含全部版本+抢单审计流水),Feign 内部接口。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Path | Long | ✅ | - | 订单 ID | | requirementType | Query | String | ❌ | HOTEL/VEHICLE/ALL | 既有参数,需求域过滤,默认 ALL | | kind | Query | String | ❌ | TRAVEL/TRANSFER | 🆕 不传=两类都返,按 kind 分组返回 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | hotelVersions | List | 房需历史版本列表,不变 | | vehicleVersions | List | 车需历史版本列表,不变 | | vehicleVersions[].kind | String | 🆕 需求类别;不传 kind 查询参数时,两类的版本序列必须分组返回、不得混在一条时间线里 | #### 请求示例 ```json GET /v3/internal/order/orders/60123456789013/requirement-history?requirementType=VEHICLE&kind=TRANSFER ``` #### 响应示例 ```json { "code": 200, "data": { "hotelVersions": [], "vehicleVersions": [ { "kind": "TRANSFER", "serviceDates": ["2026-09-11", "2026-09-18"] } ] }, "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 200, "data": {"hotelVersions": [], "vehicleVersions": []}, "success": true } ``` #### 错误响应 ```json { "code": 809000, "message": "用车需求类别非法", "success": false } ``` #### 业务边界 - 改前一条车需求版本链;改后至多两条,各自从 v1 起 - 不传 kind 参数时两类的版本序列必须分组返回,不得混在一条时间线里 ### 3. 查询订单车需求(内部) `GET /v3/internal/order/orders/{orderId}/requirement/vehicle` **VO**: `Long orderId, String kind → Result` #### 使用场景 fleet 拉取该订单当前生效用车需求,装配 `VehicleRequirementForFleetDTO`,供 fleet 看板核对与换车/改版 reconcile。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Path | Long | ✅ | - | 订单 ID | | kind | Query | String | ❌ | TRAVEL/TRANSFER | 🆕 默认 TRAVEL;指定拉取哪一类需求 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | requirementId/version/isActive/status/manualUrgent/orderCancelled/dispatchable/serviceStartDate/serviceEndDate/serviceDates/headcount/pickupRequired/dropoffRequired/submittedAt/fleet/specialTags/reassignReasons/remark | - | 既有字段,结构不变 | | kind | String | 🆕 需求类别;服务端按 (orderId, kind) 定位需求 | #### 请求示例 ```json GET /v3/internal/order/orders/60123456789013/requirement/vehicle?kind=TRANSFER ``` #### 响应示例 ```json { "code": 200, "data": { "requirementId": "1934567890123456790", "version": 1, "isActive": true, "status": "PENDING", "manualUrgent": false, "orderCancelled": false, "dispatchable": true, "serviceStartDate": "2026-09-11", "serviceEndDate": "2026-09-18", "serviceDates": ["2026-09-11", "2026-09-18"], "headcount": 6, "pickupRequired": true, "dropoffRequired": true, "submittedAt": "2026-09-10 10:05:00", "fleet": [{"vehicleType": "中巴", "seats": 35, "count": 1}], "specialTags": ["中文司机"], "reassignReasons": [], "remark": "", "kind": "TRANSFER" }, "success": true } ``` #### 空数据 / 降级响应 无(正文未明确该订单无对应 kind 当前生效需求时的具体返回形态,见交付说明留空条目)。 #### 错误响应 ```json { "code": 809000, "message": "用车需求类别非法", "success": false } ``` ```json { "code": 809007, "message": "接送机需求的服务日尚未回填,requirementId=1934567890123456790", "success": false } ``` #### 业务边界 - 按 (orderId, kind) 定位需求 - ⚠️ 分工边界:本单只引入 kind 参数与响应字段;V-4(#7443)在此基础上再加 groupBatchId - `TRANSFER` 需求的 `service_dates` 为 NULL 或空数组时被本端点消费会触发 809007(存量迁移行未回填即被取用),失败关闭;与 809002(提交时派生不出)分工不同 ### 4. 回填接送机服务日(内部,🆕 新增) `POST /v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill` **VO**: `Long orderId → Result` #### 使用场景 把 `service_dates` 为 NULL、`status=PENDING_REVIEW` 的活跃 `TRANSFER` 需求用大交通计划补齐日期并推进到 `PENDING`。v7 后本单自身不再产生这种行,端点仍交付供 `#7443` 展开前置与运维批量补数脚本调用。不经 JWT,依赖网络隔离。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Path | Long | ✅ | - | 订单 ID | 请求体:无。 #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | requirementId | Long | 被处理的活跃 TRANSFER 需求 ID;该订单无活跃 TRANSFER 需求时为 null | | outcome | String | BACKFILLED(本次补齐并推 PENDING_REVIEW→PENDING)/ ALREADY_PRESENT(本来就有非空服务日,未改动)/ NO_TRANSPORT(大交通派生不出日期,未写未推状态)/ NOT_FOUND(无活跃 TRANSFER 需求) | | serviceDates | List | 补齐后的服务日;NO_TRANSPORT/NOT_FOUND 时为空数组 | #### 请求示例 ```json POST /v3/internal/order/orders/60123456789013/requirement/transfer/service-dates/backfill ``` #### 响应示例 ```json { "code": 200, "data": { "requirementId": "1934567890123456790", "outcome": "BACKFILLED", "serviceDates": ["2026-09-11", "2026-09-18"] }, "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 200, "data": {"requirementId": null, "outcome": "NOT_FOUND", "serviceDates": []}, "success": true } ``` #### 错误响应 不新增错误码;`NO_TRANSPORT` 是正常返回态不是异常(客人还没填大交通,属业务待办不是系统故障),本端点不过 809007 守卫。以下示例是订单本身不存在时推测复用的同域既有错误码(`RequirementErrorCode.ORDER_NOT_FOUND`),工单正文未明确交代这一点,见交付说明留空条目: ```json { "code": 582001, "message": "订单不存在", "success": false } ``` #### 业务边界 - 幂等:连调两次第二次必返 ALREADY_PRESENT,`service_dates` 与 `status` 均不再变化 - 本端点是 TRANSFER 消费链路的第一步、不是兜底步:调用方须先无条件调本端点,按 outcome 分流后再调 `GET .../requirement/vehicle?kind=TRANSFER`;不得倒过来先 GET 判空再回填(迁移产生的 NULL 日期行会在 GET 那一步被 809007 打掉,永远走不到回填) - 拿到 NO_TRANSPORT 的调用方必须就地失败关闭、不得再调 GET - 本端点只补迁移遗留的空日期,不是「日期同步接口」;已有非空 `service_dates` 的行一律 ALREADY_PRESENT 原样返回,不会拿最新大交通覆盖(改签要跟上必须走换版重提) --- ## 四、契约约束与正确调用方式 | 场景 | 结果 | |------|------| | 查询 TRANSFER,`serviceDates` 为空 | 返 809007(迁移遗留行,必须先走回填端点) | | 回填后 `outcome=NO_TRANSPORT` | 调用方就地失败关闭,**不得**再调 GET | | 回填后 `outcome=BACKFILLED` / `ALREADY_PRESENT` | 可继续调 `GET .../requirement/vehicle?kind=TRANSFER` | | 取当前生效需求不带 `kind` | 双需求订单上语义不确定,**一律显式传** | **正确调用顺序**(TRANSFER 消费链路): 1. 先无条件 `POST .../requirement/transfer/service-dates/backfill` 2. 按 `outcome` 分流:`NO_TRANSPORT` 就地失败;其余继续 3. 再 `GET .../requirement/vehicle?kind=TRANSFER` ⚠️ **不得倒过来**先 GET 判空再回填:迁移产生的 NULL 日期行会在 GET 那一步被 809007 打掉,永远走不到回填。 --- ## 五、数据库行为 **对调用方无感**(不改任何接口契约),但内部消费方必须知道取数语义: - `order_vehicle_requirement` **新增两列**:`requirement_kind`(需求类别)与 `active_kind`(活跃标识, 只在行处于活跃状态时取值,失活即清空——唯一键靠它区分「当前活跃版本」与历史版本) - **新增三张团期车务需求表**:`order_group_vehicle_requirement` / `order_group_vehicle_group` / `order_group_vehicle_group_day`(团-分组-日 三层结构,本单只建表与写入口径,消费方在后续工单) - **无删表、无改列类型、无存量数据迁移**——原计划的迁移脚本 `V20260910_406`(把勾了接送机的 `TRAVEL` 行 迁成 `TRANSFER` 行)**已从本单移除**,所以**存量行不会被本单改动**,这正是回填端点存在的原因 --- ## 六、边界行为 | 错误码 | 触发条件 | 调用方该怎么处置 | |--------|----------|------------------| | **809000** | `kind` 传了 TRAVEL / TRANSFER / ALL 之外的值 | 参数错误;`ALL` 仅在需求池端点(本册 1)可用,另两个读端点不收 `ALL` | | **809002** | 回填时该单**大交通信息为空**,服务日派生不出来 | 不是故障;回填端点会以 `outcome=NO_TRANSPORT` 正常返回,调用方就地关闭该条链路 | | **809007** | 消费一条 `TRANSFER` 需求时它的 `service_dates` 仍为空 | **调用顺序错了**:先调回填端点再 GET。重试 GET 不会变好 | --- ## 六.5、枚举 ### kind | 值 | 说明 | |----|------| | TRAVEL | 行程用车 | | TRANSFER | 接送机 | | ALL | **仅需求池端点(本册 1)可用**的过滤值,取两类;不是需求类别本身 | ### BackfillResultEnum | 值 | 说明 | |----|------| | BACKFILLED | 成功回填 | | ALREADY_PRESENT | 已存在无需重复 | | NO_TRANSPORT | 客人未填大交通 | --- ## 六.6、修改前后对比 | 维度 | 改前 | 改后 | |------|------|------| | 按 orderId 取需求 | 唯一解 | **必须带 kind**,同订单最多 2 条活跃 | | 需求池过滤 | 无 kind 概念 | 不传=TRAVEL,`TRANSFER`=接送机池,`ALL`=两类 | | 变更历史 | 单条时间线 | 按 kind 分组,两类不混线 | | TRANSFER 服务日 | 不存在该类需求 | 服务端从大交通派生;迁移遗留空值走回填端点补 | --- ## 六.7、影响评估 - **向后兼容性**:部分。不传 `kind` 的既有调用在单需求订单上行为不变;**双需求订单上必须显式传 kind** - **消费方**:hl-fleet-service 需同步适配调用顺序与 `kind` 传参 --- ## 七、不影响范围 - 对外 `/v3/admin/**` 契约(见对外分册) - 房务及其他非车务内部接口 - 单订单 TRAVEL 流程(透明升级) --- ## 八、测试环境已验证 **部署**:同对外分册——2026-09-13 13:17:42 执行 deploy-backend.sh hl-order-service-v3 hl-fleet-service hl-gateway,三次 git sync done HEAD 均为 a82367e15(dev-v3);deploy-status.sh 复核三行 HEAD 一致、BEHIND 均为 2/N(落后 origin/dev-v3 2 个提交,均未触及本服务/依赖模块)。之后合入的 PR #7631(squash 1c8984042)为纯 src/test 改动,不影响运行时,无需重新部署。 **网关隔离验证**(2026-09-13,经 https://api.test.1814.love:9443):本册 4 个端点均为 /v3/internal/**,按设计不经公网网关暴露。实测确认网关正确执行了这条隔离契约——4 个路径经网关请求,一律返回 HTTP 403,响应体 {"code":403,"msg":"接口不可访问"}(hl-gateway 的 JwtAuthFilter.isInternalPath 硬编码拦截 /v3/internal/**,不看是否带 JWT)。这是符合本文件 "一、背景" 与 status_note 所述"网关不暴露该前缀"设计的正确行为,不算路由失败。 **业务逻辑验证**(绕过网关,直连测试服 hl-order-service-v3 主端口 8086,携带与真实 Feign 调用同款的 X-Internal-Token;该 token 取自 Nacos test 命名空间 hl-common-test.yml 里 internal.auth.token 的默认值 hl-internal-service-token-2026-04,测试服未见 INTERNAL_AUTH_TOKEN 环境变量覆盖,即为生效值)。直连 localhost:8086 若不带该 token,服务自身的 InternalAuthInterceptor(hl-common-web)同样返回 403 内部接口禁止外部访问,与网关层隔离是两道独立防线,均已验证生效: | # | 方法 | 路径 | HTTP | code | 关键字段/message | 判定 | |---|------|------|------|------|------------------|------| | 1 | GET | /v3/internal/order/vehicle-requirements?status=PENDING&kind=TRANSFER | 200 | 200 | 返回 4 条真实 TRANSFER 需求,逐条含 kind=TRANSFER | 路由通、业务逻辑通 | | 1' | GET | 同上,kind=BOGUS(非法值) | 200 | 809000 | 用车需求类别非法:BOGUS | 路由通、业务逻辑通 | | 2 | GET | /v3/internal/order/orders/{orderId}/requirement-history?requirementType=VEHICLE&kind=TRANSFER(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务逻辑通 | | 3 | GET | /v3/internal/order/orders/{orderId}/requirement/vehicle?kind=TRANSFER(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务逻辑通 | | 4 | POST | /v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill(不存在orderId) | 200 | 200 | outcome=NOT_FOUND, requirementId=null, serviceDates=[] | 路由通、业务逻辑通;见下方说明 | 4/4 端点直连服务均返回真实业务响应(非网关层拦截、非 404/502)。造数声明:端点 1 为纯读,未改数据;端点 2/3/4 均使用不存在的订单 ID,服务在写操作前即判定订单不存在或需求不存在而返回,未产生任何真实数据行,无需 remark 溯源标记。 补充说明(澄清正文已标注的不确定项,非新增缺陷):端点 4(回填)"错误响应"小节原本对"订单不存在"场景标注为推测复用同域既有错误码、工单正文未明确交代。实测结果是:传入不存在的订单 ID,端点不报错,而是把它当作"该订单当前无活跃 TRANSFER 需求",以 outcome=NOT_FOUND(requirementId=null)正常返回,与端点本身文档里"NOT_FOUND(无活跃 TRANSFER 需求)"的定义一致——即本端点不区分"订单不存在"与"订单存在但无活跃 TRANSFER 需求",两种情况都归为 NOT_FOUND。这与端点 2/3(会显式判定订单存在性、返 581007)行为不同,属于本端点设计如此,不是本次验证发现的新问题。 --- ## 九、相关历史 PR | PR | Issue | 说明 | |----|-------|------| | [#7551](https://git.1814.love:8443/wx/HL/pulls/7551) | #7439 | 本单主体,2026-09-11 squash 合并进 dev-v3(合并提交 `f035b85be`) | | [#7572](https://git.1814.love:8443/wx/HL/pulls/7572) | #7439 | 结算侧三条缺陷修复(合并提交 `600430303`),不触本册端点 | | [#7595](https://git.1814.love:8443/wx/HL/pulls/7595) | #7439 | **本册 1、2 的契约靠它才真的成立**(合并提交 `fdebc58c4`):修前 `kind=ALL` 抛 809000、变更历史不传 kind 静默只返 TRAVEL——实现与本文件描述不符,已订正为文件所写的口径 | | [#7597](https://git.1814.love:8443/wx/HL/pulls/7597) | #7439 | 结算响应补 `requirementKind`(合并提交 `bc75393fd`),不触本册端点 | --- ## 十、相关文档 - Issue: [#7439](https://git.1814.love:8443/wx/HL/issues/7439) - 对外分册: `changelogs-v2/2026-09/13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md` - PR: [#7551](https://git.1814.love:8443/wx/HL/pulls/7551) / [#7572](https://git.1814.love:8443/wx/HL/pulls/7572) / [#7595](https://git.1814.love:8443/wx/HL/pulls/7595) ## 关联 / 联系人 ### 链接 - **Issue**: [#7439](https://git.1814.love:8443/wx/HL/issues/7439) - **PR**: [#7551](https://git.1814.love:8443/wx/HL/pulls/7551)(已合并,`f035b85be`)、[#7595](https://git.1814.love:8443/wx/HL/pulls/7595)(已合并,`fdebc58c4`) ### 联系人 - **后端负责人**: @wx - **消费方**: hl-fleet-service