diff --git a/changelogs-v2/2026-09/13_7439_团期车务地基-内部接口-修改接口-管理后台.md b/changelogs-v2/2026-09/13_7439_团期车务地基-内部接口-修改接口-管理后台.md new file mode 100644 index 00000000..ca6db7c2 --- /dev/null +++ b/changelogs-v2/2026-09/13_7439_团期车务地基-内部接口-修改接口-管理后台.md @@ -0,0 +1,516 @@ +--- +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 diff --git a/changelogs-v2/2026-09/13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md b/changelogs-v2/2026-09/13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md new file mode 100644 index 00000000..4bad95c5 --- /dev/null +++ b/changelogs-v2/2026-09/13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md @@ -0,0 +1,802 @@ +--- +schema: "hl-changelog/v2" +ticket: "7439" +title: "团期车务地基——用车需求按 kind 分家、服务日派生、手录行双身份" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-13T14:20:00" +status_note: "本文件是 #7439 的对外分册(7 个 /v3/admin 端点)。4 个 /v3/internal 端点已按 BACKEND_CHANGELOG_DELIVERY_GUIDE.md 2.5 节拆出为内部分册 13_7439_团期车务地基-内部接口-修改接口-管理后台.md,两份同批交付。" +updated_at: "2026-09-13" +base: "dev-v3" +--- + +# order-v3: 团期车务地基 + +**服务**: hl-order-service-v3 +**PR**: #7551(`f035b85be`)、#7572(`600430303`)、#7595(`fdebc58c4`)、#7597(`bc75393fd`) +**Issue**: #7439 + +> **本文件是分册,不是全部。** #7439 共改 11 个端点,本册是面向 hl-ui 的 7 个对外端点 +> (`/v3/admin/**`);另外 4 个 `/v3/internal/**` Feign 端点在 `13_7439_团期车务地基-内部接口-修改接口-管理后台.md`。 +> 拆分依据交付指南 2.5 节「`/v3/internal/*` 必须单独成篇,不许和 admin/mp 接口塞同一份」。 +> **前端无需读那一册**——那 4 个端点前端调不到。 + +--- + +## ⚠️ 关键变化 + +本单为团期车务打地基: + +1. **用车需求按 kind 分家**:同订单可并存 TRAVEL(行程)与 TRANSFER(接送机)两类活跃需求,各自独立流转状态与版本序列 +2. **服务日服务端派生**:TRANSFER 的 service_dates 由大交通自动派生,前端不传也不能传 +3. **手录行双身份归属**:结算手录车费行新增 requirementKind 字段(双需求时必填) +4. **配车写口 kind 感知**:提交/放行/打回等操作都新增 kind 参数 + +**关键限制**:本单不产生 TRANSFER 行;开关 hl.order.requirement.transfer-kind-submit-enabled 默认 false;提交返 809009。 + +--- + +## 一、背景 + +团期车务从单一用车需求(行程)扩展到双需求并存(+接送机)。本单涉及定制师端口、团期管理员端口、车控配车端口、结算查看端口、内部派生接口的 kind 感知改造。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 提交/修改用车需求 | PUT | `/v3/admin/order/{id}/vehicle-requirement` | 改造 | 定制师提交/修改/调整需求,请求体新增可选 kind | +| 2 | 放行用车需求 | POST | `/v3/admin/order/{id}/vehicle-requirement/dispatch` | 改造 | 团期管理员按 kind 放行至车务 | +| 3 | 打回用车需求(定制师) | POST | `/v3/admin/order/{id}/vehicle-requirement/reject` | 改造 | 团期管理员按 kind 打回定制师 | +| 4 | 打回用车需求(车控) | POST | `/v3/admin/order/{id}/vehicle-requirement/supplier-reject` | 改造 | 车控按 kind 打回上游 | +| 5 | 我的接单列表 | GET | `/v3/admin/order/grab-pool/my-claims/vehicle` | 改造 | 列表新增 kind 过滤与回显 | +| 6 | 车辆核单——保存 | PUT | `/v3/admin/order/{orderId}/settlement/step3/vehicles` | 改造 | 手录行新增 requirementKind 归属字段 | +| 7 | 车辆核单——查看 | GET | `/v3/admin/order/{orderId}/settlement/step3/vehicles` | 改造 | 回显逐行新增 requirementKind | + +--- + +## 三、接口详情 + +### 1. 提交/修改用车需求 `PUT /v3/admin/order/{id}/vehicle-requirement` + +**VO**: `VehicleRequirementReqVO → Result` + +#### 使用场景 + +定制师在后台提交、修改、调整团期或核心订单的用车需求。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | - | 订单 ID | +| kind | Body | String | ❌ | TRAVEL/TRANSFER | 需求类型,默认 TRAVEL;非法值实际返 400(Bean Validation 拦截,不是 809000,见下方错误响应说明) | +| fleet | Body | List | ✅ | @NotEmpty | 车型配置,两类需求都必填 | +| pickupRequired | Body | Boolean | ❌ | - | 需接机 | +| dropoffRequired | Body | Boolean | ❌ | - | 需送机 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| kind | String | 本次落库的 kind(TRAVEL/TRANSFER) | +| serviceDates | List | 派生出的服务日;TRAVEL=行程日,TRANSFER=航班日(可落在行程窗外) | + +#### 请求示例 + +```json +{ + "kind": "TRANSFER", + "fleet": [{"vehicleType": "中巴", "seats": 35, "count": 1}] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "kind": "TRANSFER", + "serviceDates": ["2026-09-11", "2026-09-18"], + "fleet": [{"vehicleType": "中巴", "seats": 35, "count": 1}] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ + "code": 400, + "message": "用车需求类别只能是 TRAVEL 或 TRANSFER", + "success": false +} +``` + +本端点非法 kind 实测返回的是上面这个 400,不是 809000。原因:VehicleRequirementReqVO.kind 字段带 Pattern 校验(regexp=TRAVEL|TRANSFER,见 VehicleRequirementReqVO.java 第27行),Bean Validation 在到达业务代码之前就已拦截,809000 分支对本端点的 body 参数不可达(2026-09-13 网关实测确认)。809000 这个业务错误码本身仍然存在,走的是同一处校验方法 VehicleRequirementKind.of():内部接口 GET /v3/internal/order/vehicle-requirements?kind=BOGUS 实测会返回它(见内部接口分册端点1)。同一处校验逻辑在两个不同入口有两种不同表现——走 Body+Bean Validation 的入口(本端点)只会看到 400,走 Query+业务层解析的入口才会看到 809000。mmg 若两个入口都对接,两条分支都要处理。 + +```json +{ + "code": 809001, + "message": "订单已存在同类型的活跃需求,请勿并发提交", + "success": false +} +``` + +```json +{ + "code": 809002, + "message": "接送机服务日派生失败:大交通计划未填写或时间为空", + "success": false +} +``` + +```json +{ + "code": 809009, + "message": "接送机需求提交尚未开放,请稍候", + "success": false +} +``` + +#### 业务边界 + +- 同订单可并存两类活跃需求 +- 服务日随版本冻结,改签需重新提交 + +### 2. 放行用车需求 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch` + +**VO**: `Long + DispatchReqVO → Result` + +#### 使用场景 + +团期管理员把用车需求提交给车务处理。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | - | 子订单 ID | +| kind | Query | String | ❌ | TRAVEL/TRANSFER | 指定放行哪一类,默认 TRAVEL | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| - | - | 无返回体(`Result`),仅以 HTTP 层 `success` 标记成败 | + +#### 请求示例 + +```json +POST /v3/admin/order/1234567890123456789/vehicle-requirement/dispatch?kind=TRANSFER +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ + "code": 809000, + "message": "用车需求类别非法", + "success": false +} +``` + +#### 业务边界 + +- 放行指定 kind,另一类不受影响 + +### 3. 打回用车需求(定制师) `POST /v3/admin/order/{id}/vehicle-requirement/reject` + +**VO**: `RejectReqVO → Result` + +#### 使用场景 + +团期管理员在后台打回定制师提交的车需求(`PENDING_REVIEW` / `PENDING` → `REJECTED_TO_CONSULTANT`),仅对团期订单生效。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | - | 订单 ID | +| returnRemark | Body | String | ✅ | @NotBlank,≤500字符 | 打回备注(定制师重新提交时会创建新需求) | +| kind | Query | String | ❌ | TRAVEL/TRANSFER | 🆕 指定打回哪一类需求,默认 TRAVEL;注意:不在请求体 RejectReqVO 里,必须拼进 URL 的 query string;放进 JSON body 会被静默忽略,不报错也不返回 400,服务端按默认值 TRAVEL 处理 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| - | - | 无返回体(`Result`),仅以 HTTP 层 `success` 标记成败 | + +#### 请求示例 + +```http +POST /v3/admin/order/60123456789013/vehicle-requirement/reject?kind=TRANSFER +``` + +```json +{ + "returnRemark": "行程未安排司机休息时间,请重新确认车型" +} +``` + +注意:kind 必须拼进 URL 的 query string(如上),不能放进 JSON body。RejectReqVO 没有 kind 字段,放进 body 会被 Jackson 静默忽略、服务端按默认值 TRAVEL 处理——不报错、不返回 400,打回操作可能悄悄落到错误的需求类型上(2026-09-13 核实代码:VehicleRequirementAdminController.java 的 rejectVehicleRequirement 用 @RequestParam(defaultValue="TRAVEL") String kind 接收;RejectReqVO.java 只有 returnRemark 一个字段)。 + +#### 响应示例 + +```json +{ + "code": 200, + "data": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ + "code": 809000, + "message": "用车需求类别非法", + "success": false +} +``` + +```json +{ + "code": 582083, + "message": "需求状态不允许此操作,请检查当前状态", + "success": false +} +``` + +#### 业务边界 + +- 仅对团期订单生效,订单非团期或状态非 `PENDING_REVIEW`/`PENDING` 时返 582083(`REQUIREMENT_STATUS_TRANSITION_INVALID`) +- 打回同时清团级 `requirement_confirmed` 确认标记并写团级时间线 `BATCH_REQUIREMENT_REJECT` +- 两类需求各自独立流转态、各自版本序列,打回一类不影响另一类 + +### 4. 打回用车需求(车控) `POST /v3/admin/order/{id}/vehicle-requirement/supplier-reject` + +**VO**: `RejectReqVO → Result` + +#### 使用场景 + +车控配车面板打回车需求(配不出退上游)。`returnTarget` 按订单类型服务端自动派生:核心订单→定制师,团期订单→团期管理员。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | - | 订单 ID | +| returnRemark | Body | String | ✅ | @NotBlank,≤500字符 | 打回备注 | +| kind | Query | String | ❌ | TRAVEL/TRANSFER | 🆕 指定打回哪一类需求,默认 TRAVEL;注意:不在请求体 RejectReqVO 里,必须拼进 URL 的 query string;放进 JSON body 会被静默忽略,不报错也不返回 400,服务端按默认值 TRAVEL 处理 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| - | - | 无返回体(`Result`),仅以 HTTP 层 `success` 标记成败 | + +#### 请求示例 + +```http +POST /v3/admin/order/60123456789013/vehicle-requirement/supplier-reject?kind=TRANSFER +``` + +```json +{ + "returnRemark": "车型库存不足,无法配出" +} +``` + +注意:kind 必须拼进 URL 的 query string(如上),不能放进 JSON body。RejectReqVO 没有 kind 字段,放进 body 会被 Jackson 静默忽略、服务端按默认值 TRAVEL 处理——不报错、不返回 400,打回操作可能悄悄落到错误的需求类型上(2026-09-13 核实代码:VehicleRequirementAdminController.java 的 supplierRejectVehicleRequirement 用 @RequestParam(defaultValue="TRAVEL") String kind 接收;RejectReqVO.java 只有 returnRemark 一个字段)。 + +#### 响应示例 + +```json +{ + "code": 200, + "data": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ + "code": 809000, + "message": "用车需求类别非法", + "success": false +} +``` + +```json +{ + "code": 582083, + "message": "需求状态不允许此操作,请检查当前状态", + "success": false +} +``` + +#### 业务边界 + +- `returnTarget` 完全由服务端按订单类型派生,前端不传、也不接受前端指定 +- 按 kind 定位待打回的那条需求,另一 kind 的需求状态不受影响 + +### 5. 我的接单列表 `GET /v3/admin/order/grab-pool/my-claims/vehicle` + +**VO**: `MyClaimsQueryReqVO → Result>` + +#### 使用场景 + +车控视角"我的接单"分页列表;`claimerId` 由后端从 JWT 派生,前端不传。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| status | Query | String | ❌ | PROCESSING/DONE/ALL | 状态过滤,默认 ALL | +| page | Query | Integer | ❌ | ≥1,默认1 | 页码 | +| pageSize | Query | Integer | ❌ | 1-100,默认20 | 每页条数 | +| kind | Query | String | ❌ | TRAVEL/TRANSFER | 🆕 不传=不过滤,两类都返 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| records | List | 记录列表,既有字段不变:requirementId/orderId/productName/vehicleTypeSummary/specialTags/submittedAt/consultantName/consultantId/status/claimedAt/isAdjustment | +| total | Integer | 总条数 | +| page | Integer | 当前页码 | +| pageSize | Integer | 每页条数 | +| records[].kind | String | 🆕 需求类别(TRAVEL/TRANSFER) | + +#### 请求示例 + +```json +GET /v3/admin/order/grab-pool/my-claims/vehicle?status=PROCESSING&page=1&pageSize=20&kind=TRANSFER +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "records": [ + { + "requirementId": 90022334455, + "orderId": 60123456789013, + "productName": "长白山自由行5日", + "vehicleTypeSummary": "商务车×1", + "specialTags": ["儿童安全座椅"], + "submittedAt": "2026-09-10T14:35:10", + "consultantName": "陈定制师", + "consultantId": "10086", + "status": "PROCESSING", + "claimedAt": "2026-09-10T16:20:00", + "isAdjustment": false, + "kind": "TRANSFER" + } + ], + "total": 1, + "page": 1, + "pageSize": 20 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"records": [], "total": 0, "page": 1, "pageSize": 20}, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 809000, + "message": "用车需求类别非法", + "success": false +} +``` + +#### 业务边界 + +- 不传 kind 时两类需求混列返回,前端需按 kind 分栏或加筛选(改前列表只可能是行程用车,改后可能混入接送机需求) +- `claimerId` 由后端从 JWT 派生,不接受前端指定他人 + +### 6. 车辆核单——保存 `PUT /v3/admin/order/{orderId}/settlement/step3/vehicles` + +**VO**: `SettlementVehicleFeesSaveReqVO → Result` + +#### 使用场景 + +结算侧全量保存车辆核单草稿(含运营手录行),R3c/D-A27① 改造。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Path | Long | ✅ | @Min(1) | 订单 ID | +| items | Body | List | ✅ | @Valid @NotNull | 全量明细,覆盖式保存 | +| items[].id | Body | Long | ❌ | @Positive | 明细行 ID | +| items[].sourceType | Body | String | ✅ | FLEET/MANUAL | 来源类型 | +| items[].serviceDate | Body | LocalDate | ✅ | @NotNull | 服务日期 | +| items[].vehicleId | Body | Long | ❌ | @Positive | 车辆 ID | +| items[].vehiclePlate | Body | String | ❌ | ≤64字符 | 车牌 | +| items[].vehicleModelId | Body | Long | ❌ | @Positive | 车型 ID | +| items[].vehicleModelName | Body | String | ❌ | ≤128字符 | 车型名 | +| items[].driverId | Body | Long | ❌ | @Positive | 司机 ID | +| items[].driverName | Body | String | ❌ | ≤64字符 | 司机姓名 | +| items[].amount | Body | BigDecimal | ✅ | ≥0.00,整数10位小数2位 | 车费金额 | +| items[].paymentMethod | Body | String | ✅ | CASH_PAID/SIGNED/COMPANY_PAID | 付款方式 | +| items[].settlementConfirmStatus | Body | String | ✅ | UNCONFIRMED/CONFIRMED | 核单确认状态 | +| items[].remark | Body | String | ❌ | ≤500字符 | 备注 | +| items[].voucherUrls | Body | List | ❌ | ≤9条,每条≤1024字符,需 http/https 开头 | 凭证URL | +| items[].requirementKind | Body | String | 条件必填 | TRAVEL/TRANSFER | 🆕 需求归属:`sourceType=MANUAL` 且该单两类活跃需求并存时必填(缺失返 809008);只有一条活跃需求时可省,服务端从实际活跃身份正向解析唯一解;`sourceType=FLEET` 时必须不传 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | Long | 订单ID(字符串序列化,避免精度丢失) | +| totalAmount | BigDecimal | 当前明细总金额 | +| allConfirmed | Boolean | 非空行是否全部已确认;合法空集为true | +| settlementReady | Boolean | 车辆费用是否已具备完成核单条件 | +| blockReasonCode | String | 不可完成核单的机器可读原因,可完成时为null | +| items[].id 等既有字段 | - | sourceType/sourceTypeName/serviceDate/vehicleId/vehiclePlate/vehicleModelId/vehicleModelName/driverId/driverName/amount/paymentMethod/paymentMethodName/settlementConfirmStatus/settlementConfirmStatusName/remark/voucherUrls,结构不变 | +| items[].requirementKind | String | 🆕 逐行需求归属(由 `requirement_id` 反查所属 kind;`requirement_id` 为空一律回显 TRAVEL),供下次全量提交原样带回 | + +#### 请求示例 + +```json +{ + "items": [ + { + "sourceType": "MANUAL", + "serviceDate": "2026-09-12", + "vehicleId": 1001, + "vehiclePlate": "蒙A12345", + "amount": 800.00, + "paymentMethod": "CASH_PAID", + "settlementConfirmStatus": "CONFIRMED", + "requirementKind": "TRANSFER" + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "orderId": "60123456789013", + "totalAmount": 800.00, + "allConfirmed": true, + "settlementReady": true, + "blockReasonCode": null, + "items": [ + { + "id": 90011223344, + "sourceType": "MANUAL", + "sourceTypeName": "运营手录", + "serviceDate": "2026-09-12", + "vehicleId": 1001, + "vehiclePlate": "蒙A12345", + "amount": 800.00, + "paymentMethod": "CASH_PAID", + "paymentMethodName": "现金支付", + "settlementConfirmStatus": "CONFIRMED", + "settlementConfirmStatusName": "已确认", + "requirementKind": "TRANSFER" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"orderId": "60123456789013", "totalAmount": 0, "allConfirmed": true, "settlementReady": false, "blockReasonCode": "VEHICLE_FEE_NOT_READY", "items": []}, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 809008, + "message": "手录车费行未指定需求归属,订单同时存在 TRAVEL、TRANSFER 两类活跃需求", + "success": false +} +``` + +```json +{ + "code": 584100, + "message": "车务车辆总车费暂时不可用,请稍后重试", + "success": false +} +``` + +#### 业务边界 + +- 🔴 硬契约变更:hl-ui 不传 `requirementKind` 时,只有一条活跃需求的订单仍存得进去(服务端解析唯一解),但两类需求并存的订单整批存不进去(809008),前端必须在两类需求并存时让运营选归属 +- `FLEET` 行的归属由来源决定,不接受前端指定;`MANUAL` 行指定的 kind 在本单没有活跃需求时复用 584100(`FLEET_VEHICLE_FEE_UNAVAILABLE`),与 809008 分工不同:584100=指错了、809008=没指定且有歧义 +- 严格模式:`@JsonIgnoreProperties(ignoreUnknown=false)` + `@JsonAnySetter`,多传未知字段当场被拒 +- 手录行"冻得下去"依赖另一开关 `hl.order.settlement.vehicle-fee-audit-v3-write-enabled`,为 off 时含手录行的订单调 finalize 返 584100(有意的失败关闭) + +### 7. 车辆核单——查看 `GET /v3/admin/order/{orderId}/settlement/step3/vehicles` + +**VO**: `Long → Result` + +#### 使用场景 + +结算侧查询车辆核单草稿(回显口),本单仅改造响应新增字段,查询逻辑不变。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Path | Long | ✅ | @Min(1) | 订单 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId/totalAmount/allConfirmed/settlementReady/blockReasonCode/items[] 各既有字段 | - | 结构不变,同接口6出参 | +| items[].requirementKind | String | 🆕 逐行需求归属(口径同接口6:`requirement_id` 为空一律回显 TRAVEL) | + +#### 请求示例 + +```json +GET /v3/admin/order/60123456789013/settlement/step3/vehicles +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "orderId": "60123456789013", + "totalAmount": 800.00, + "allConfirmed": true, + "settlementReady": true, + "blockReasonCode": null, + "items": [ + { + "id": 90011223344, + "sourceType": "MANUAL", + "sourceTypeName": "运营手录", + "serviceDate": "2026-09-12", + "amount": 800.00, + "paymentMethod": "CASH_PAID", + "paymentMethodName": "现金支付", + "settlementConfirmStatus": "CONFIRMED", + "settlementConfirmStatusName": "已确认", + "requirementKind": "TRANSFER" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"orderId": "60123456789013", "totalAmount": 0, "allConfirmed": true, "settlementReady": false, "blockReasonCode": "VEHICLE_FEE_NOT_READY", "items": []}, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "订单 ID 必须大于 0", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 回显里同时出现 TRAVEL 与 TRANSFER 两类行,前端须按 `requirementKind` 分栏或加筛选,并在全量提交时把该字段原样带回(MANUAL 行必带、FLEET 行不带) +- 错误码零新增,与改前完全一致 + + +--- + +## 四、契约约束与正确调用方式 + +| 场景 | 结果 | +|------|------| +| 提交 TRANSFER,客人未填大交通 | 返 809002 | +| 提交 TRANSFER,开关关闭(默认) | 返 809009,零落库 | +| 双需求手录行不指定 requirementKind | 返 809008,零落库 | + +--- + +## 五、数据库行为 + +**对前端无感**(不改任何接口契约),但为免误读,如实列出: + +- `order_vehicle_requirement` **新增两列**:`requirement_kind`(需求类别)与 `active_kind`(活跃标识,只在行处于活跃状态时取值,失活即清空——唯一键靠它区分「当前活跃版本」与历史版本) +- **新增三张团期车务需求表**:`order_group_vehicle_requirement` / `order_group_vehicle_group` / `order_group_vehicle_group_day`(团-分组-日 三层结构,本单只建表与写入口径,消费方在后续工单) +- **无删表、无改列类型、无存量数据迁移**——⚠️ 原计划的迁移脚本 `V20260910_406`(把勾了接送机的 `TRAVEL` 行迁成 `TRANSFER` 行)**已从本单移除**,所以**存量行不会被本单改动** + +> 留痕:本节初稿写的是「无表创建/删除,仅新增列 requirement_kind 到已有表」,与事实不符(漏了三张新表与 `active_kind` 列)。2026-09-11 验收取证时发现并改写。 + +--- + +## 六、边界行为 + +**每个码配一条前端处置口径——码本身不够,处置动作错了照样是线上问题。** + +| 错误码 | 触发条件 | 🖐 前端该怎么处置 | +|--------|----------|------------------| +| **809000** | `kind` 传了 TRAVEL / TRANSFER 之外的值 | 参数错误,按常规表单校验处理;正常调用不该出现 | +| **809002** | 提交 `TRANSFER`,但该单**大交通信息为空**,服务日派生不出来 | ⚠️ **不要提示「参数错误」或「稍后重试」**——这是数据缺口,必须给「**先去补大交通**」的引导,最好直接给一个跳到大交通填写页的入口。重试不会变好,补完大交通再提交才会好 | +| **809008** | 该单**两类活跃需求并存**时,手录车费行没指定 `requirementKind` | 表单必填校验漏了;把该字段做成必选(两类并存时)即可 | +| **809009** | 提交 `kind=TRANSFER`,而开关 `hl.order.requirement.transfer-kind-submit-enabled` **仍是默认的 false** | ⚠️ **按「功能未开放」提示**(例如「接送机需求提交尚未开放」),**不得**按参数错误处理、**也不得**提示「稍后重试」——开关不开,重试多少次都是这个码。本单交付时该开关就是关的,所以这是**当前的正常返回**,不是故障 | + +809000 的可达性因入口而异:端点2(放行)/3(打回·定制师)/4(打回·车控)的 kind 是 Query 参数、走业务层 VehicleRequirementKind.of() 解析,非法值会返 809000(如上表);端点1(提交/修改用车需求)的 kind 是 Body 字段且带 Pattern 校验,非法值在到达业务代码前就被拦成 400,809000 对该端点不可达,详见端点1的错误响应小节。前端不要为端点1的非法 kind 编写 809000 处置分支。 + +⚠️ 另有一条与本单无关但同页可见的既有文案问题:`584100`「车务车辆总车费暂时不可用,**请稍后重试**」(见第 6 个端点的错误响应示例)其实对应的是**永久性**数据形态,重试不会好。已另行报备,前端暂按原样透出即可。 + +--- + +## 六.5、枚举 + +### kind + +| 值 | 说明 | +|----|------| +| TRAVEL | 行程用车 | +| TRANSFER | 接送机 | + + +--- + +## 六.6、修改前后对比 + +| 维度 | 改前 | 改后 | +|------|------|------| +| 同订单活跃需求 | 最多 1 条 | 最多 2 条 | +| 服务日来源 | 行程日 | TRAVEL=行程日,TRANSFER=大交通 | +| 手录行识别 | 无需求信息 | 必须指定 kind | +| 配车操作 | 操作唯一需求 | 按 kind 指定操作对象 | + +--- + +## 六.7、影响评估 + +- **向后兼容性**:部分。不传 kind 时按 TRAVEL 处理,但双需求时手录行必须指定 +- **前端同步上线**:是 + +--- + +## 七、不影响范围 + +- 房务及其他非车务 +- 单订单 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)。经 SSH `root@192.168.100.236` 跑 `bash /opt/hulalv/scripts/deploy-status.sh` 复核(下同,内部分册引用同一次结果): + +``` +SERVICE BRANCH COMMIT BEHIND DEPLOYED_AT BY +hl-order-service-v3 dev-v3 a82367e15 2/N 2026-09-13 13:18:39 root@192.168.100.168 +hl-fleet-service dev-v3 a82367e15 2/N 2026-09-13 13:19:22 root@192.168.100.168 +hl-gateway dev-v3 a82367e15 2/N 2026-09-13 13:19:45 root@192.168.100.168 +``` + +三行 BEHIND 均为 2/N:落后 origin/dev-v3 2 个提交,N=未触及本服务/本单依赖模块。之后合入的 PR #7631(squash 1c8984042)为 4 个文件的纯 src/test 改动,不影响运行时字节,无需重新部署。 + +**网关实测**(2026-09-13 14:10 左右,账号 admin 登录后切 ADMIN 角色,经 https://api.test.1814.love:9443):除纯读端点外,一律用不存在的订单 ID 99999999999999999(或非法枚举值)触发早期业务校验,用真实业务错误码证明路由与业务代码均已触达,全程未写入或修改任何真实数据行: + +| # | 方法 | 路径 | HTTP | code | message | 判定 | +|---|------|------|------|------|---------|------| +| 1 | PUT | /v3/admin/order/{id}/vehicle-requirement(kind=BOGUS) | 200 | 400 | 用车需求类别只能是 TRAVEL 或 TRANSFER | 路由通、业务代码触达;见下方发现① | +| 2 | POST | /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRAVEL(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务代码触达 | +| 3 | POST | /v3/admin/order/{id}/vehicle-requirement/reject(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务代码触达;见下方发现② | +| 4 | POST | /v3/admin/order/{id}/vehicle-requirement/supplier-reject(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务代码触达;见下方发现② | +| 5 | GET | /v3/admin/order/grab-pool/my-claims/vehicle?kind=TRANSFER | 200 | 200 | 成功(真实查询,0 条) | 路由通、业务代码触达 | +| 6 | PUT | /v3/admin/order/{orderId}/settlement/step3/vehicles(不存在orderId,items为空) | 200 | 581007 | 订单不存在 | 路由通、业务代码触达 | +| 7 | GET | /v3/admin/order/{orderId}/settlement/step3/vehicles(不存在orderId) | 200 | 581007 | 订单不存在 | 路由通、业务代码触达 | + +7/7 端点经网关路由至 hl-order-service-v3 并返回真实业务响应(无 404/502/网关鉴权错误)。造数声明:本轮全部使用不存在的订单 ID 或空 items,服务在到达任何写操作前即因订单不存在或参数非法而拒绝,未产生任何真实数据行,因此本次验证无需写 remark 溯源标记。 + +发现的两处正文与实现不符(已在 §二/§三直接订正正文,不再另存一份可能漂移的描述): + +1. 端点 1(提交/修改用车需求)的非法 kind 实际返回 400,不是原正文写的 809000——§三端点1 的入参表与错误响应小节已改成实测结果,并说明了 809000 为什么对本端点不可达。§六边界行为表也同步加了这条分入口说明。 +2. 端点 3(打回·定制师)与端点 4(打回·车控)的 kind 实际是 Query 参数、不在 RejectReqVO 里——§三对应端点的入参表、请求示例已改正,并显式警告 kind 放进 JSON body 会被静默忽略、服务端按默认值 TRAVEL 处理(不报错、不返回 400)。 + +--- + +## 九、相关历史 PR + +| PR | Issue | 说明 | +|----|-------|------| +| [#7551](https://git.1814.love:8443/wx/HL/pulls/7551) | #7439 | 本单主体,2026-09-11 squash 合并进 dev-v3(合并提交 `f035b85be`);⚠️ 初稿误填过 #7504,那是 #7446 的 PR | +| [#7572](https://git.1814.love:8443/wx/HL/pulls/7572) | #7439 | 结算侧三条缺陷修复(合并提交 `600430303`):本册端点 6 / 7 的手录行双身份归属靠它才完整 | +| [#7595](https://git.1814.love:8443/wx/HL/pulls/7595) | #7439 | 需求域两条契约缺陷修复(合并提交 `fdebc58c4`):影响的是内部分册的 1、2 两个端点,本册契约不变 | +| [#7597](https://git.1814.love:8443/wx/HL/pulls/7597) | #7439 | **本册端点 6 / 7 的 `items[].requirementKind` 靠它才真的返回**(合并提交 `bc75393fd`):修前响应 VO 里根本没有这个字段,而本文件出参表与响应示例都写了它 | + +--- + +## 十、相关文档 + +- 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)(均已合并,当前基线 `fdebc58c4`) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7439](https://git.1814.love:8443/wx/HL/issues/7439) +- **PR**: [#7551](https://git.1814.love:8443/wx/HL/pulls/7551)(已合并,`f035b85be`)、[#7572](https://git.1814.love:8443/wx/HL/pulls/7572)(已合并,`600430303`)、[#7595](https://git.1814.love:8443/wx/HL/pulls/7595)(已合并,`fdebc58c4`) + +### 联系人 + +- **后端负责人**: @wx