diff --git a/changelogs-v2/2026-09/28_8464_派单看板加订单归属页签与团期配车菜单下线-修改接口-管理后台.md b/changelogs-v2/2026-09/28_8464_派单看板加订单归属页签与团期配车菜单下线-修改接口-管理后台.md new file mode 100644 index 00000000..b89ab420 --- /dev/null +++ b/changelogs-v2/2026-09/28_8464_派单看板加订单归属页签与团期配车菜单下线-修改接口-管理后台.md @@ -0,0 +1,756 @@ +--- +schema: "hl-changelog/v2" +ticket: "8464" +title: "派单看板两个读口新增订单归属过滤 orderKind,团期配车菜单下线" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "" +updated_at: "2026-09-28" +base: "dev-v3" +--- + +# hl-fleet-service: 派单看板加订单归属页签 `orderKind`,团期配车菜单下线 + +**服务**: hl-fleet-service(菜单行由 hl-user-service 下发) +**PR**: #8473(fleet,已合入 `dev-v3`,squash `ad4e65a27`)/ #8474(user 菜单,已合入 `dev-v3`,squash `f49ae8d40`) +**Issue**: #8464 +**日期**: 2026-09-28 +**影响范围**: 管理后台车务「派单看板」的列表与汇总两个读口;以及「团期配车」独立菜单入口 + +--- + +## ⚠️ 关键变化 + +- `GET /admin/fleet/board/orders` 与 `GET /admin/fleet/board/summary` **新增可选查询参数 `orderKind`**,取值 `ALL` / `NORMAL` / `GROUP`,判据是「这条派车需求当前属不属于某个运营团期」。 +- **不传或传空串 = `ALL`(不过滤)**。这与订单列表 `GET /v3/admin/order` 的 `orderKind` **同名同取值但缺省相反**(那边不传缺省 `NORMAL`)。两个页面的筛选状态不能直接互相透传,详见「四、契约约束与正确调用方式」。 +- **非法取值不被静默容忍**:大小写不符(如 `group`)或取值域外(如 `XX`)一律 **HTTP 200 + body `code=100001`**,`data=null`、`success=false`。 +- **汇总接口的 `consultantOptions`、`todayDepartCount`、`pendingCount`、`pendingUrgentCount`、`statusCounts`、`statusOptions[].count` 随 `orderKind` 一起收窄**——它们是「当前筛选范围内」的分面,不是全局统计。**只有 `idleVehicleCount` / `idleDriverCount` 是全局物理资源指标,切页签时这两个数字不会变**(实测三档恒为 48 / 51)。 +- **「团期配车」独立菜单行已删除**,管理后台的 `/fleet/group-dispatch` 路由不再注册,`src/views/fleet/group-dispatch/` 整个模块失去唯一入口。前端要做的处置见「四 → 团期配车模块处置」。 + +--- + +## 一、背景 + +车务原本有两个并列菜单:派单看板(逐条用车需求)与团期配车(一团一行)。#7443 已给看板加了 `groupBatchId` 运营团期**精确**筛选(等值匹配某一个团)。本次补的是**粗筛**这一档——「只看普通订单」或「只看团期订单」,车务不必在两个菜单之间来回切。 + +粗细两档用的是同一个归属判定(`GroupBatchOwnershipResolver`):**订单上下文的实时归属优先,order-v3 整体不可达时才回退派车行上的建行快照**。所以 `orderKind=GROUP` 与 `groupBatchId=<某团>` 对同一批行给出一致的口径,不会出现「按团筛出来的行自己显示成非团」。 + +| 维度 | 粗筛 `orderKind` | 精筛 `groupBatchId` | +|------|------------------|---------------------| +| 问题 | 属不属于团期 | 属不属于**这一个**团期 | +| 类型 | `String` 常量 | `Long`(雪花,字符串透传) | +| 组合 | 与 `groupBatchId` 按 AND 组合 | 同左 | + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 派单看板列表 | GET | `/admin/fleet/board/orders` | 新增可选入参 | 新增 `orderKind`,缺省 `ALL`;非法值返 `100001` | +| 2 | 派单看板汇总 | GET | `/admin/fleet/board/summary` | 新增可选入参 + 汇总口径 | 同上;且 `consultantOptions` / `todayDepartCount` 等分面随之收窄 | + +--- + +## 三、接口详情 + +### 1. 派单看板列表 `GET /admin/fleet/board/orders` + +**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO` + +#### 使用场景 + +车务「派单看板」主列表。本次在原有筛选条件上加一个订单归属页签(全部 / 常规订单 / 团期订单):切页签时把 `orderKind` 带上重新拉列表;选「全部」时可以不带该参数,与显式传 `ALL` 等价。 + +维度不变:**同一 `requirementId` 只返回一条 record**,`dailyAssignments` 保留逐日派车行;顶层兼容字段指向最需要处理的代表日行。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderKind | Query | String | ❌ | `ALL` / `NORMAL` / `GROUP`,**大小写敏感**;其余值返 `100001` | **本次新增**。订单归属粗筛。不传或空串 = `ALL` 不过滤;`NORMAL` = 当前无团期归属;`GROUP` = 当前属于某个运营团期。首尾空白会被去掉后再判定 | +| groupBatchId | Query | Long | ❌ | 雪花 ID,字符串透传禁 `Number()` | 运营团期精确筛(#7443)。与 `orderKind` 按 AND 组合 | +| statuses | Query | String[] | ❌ | `unassigned` / `unassigned_urgent` / `holding` / `holding_urgent` / `assigned` / `canceled` / `completed` | 多状态筛选,任一命中即返;空 = 不按状态过滤 | +| status | Query | String | ❌ | 同 `statuses` 取值 | 别名,单值或逗号分隔,与 `statuses` 合并 | +| startDayFrom | Query | String | ❌ | `YYYY-MM-DD` | 日期区间起,与行程区间 `[startDate,endDate]` **重叠**(不是仅出团日);单边只约束一侧 | +| startDayTo | Query | String | ❌ | `YYYY-MM-DD` | 日期区间止,同上 | +| startDate | Query | String | ❌ | `YYYY-MM-DD` | `startDayFrom` 的别名,未传 `startDayFrom` 时生效 | +| endDate | Query | String | ❌ | `YYYY-MM-DD` | `startDayTo` 的别名,未传 `startDayTo` 时生效 | +| vehicleTypeKeys | Query | String[] | ❌ | `suv` / `mpv` / `bus` / `sedan` | 车型大类多选;未派按需求车型、已派按实际车辆大类过滤 | +| typeKeys | Query | String[] | ❌ | 同上 | `vehicleTypeKeys` 的别名,未传时生效 | +| driverName | Query | String | ❌ | - | 司机姓名模糊搜索 | +| keyword | Query | String | ❌ | - | 统一文字搜索:司机 / 联系人客户 / 团号 / 订单号 / 定制师显示名,任一包含即命中 | +| contactName | Query | String | ❌ | - | 联系人、客户名模糊搜索 | +| contactKeyword | Query | String | ❌ | - | `contactName` 的别名 | +| teamNo | Query | String | ❌ | - | 团号模糊搜索,只匹配真实团号、不匹配订单号 | +| consultantId | Query | Long | ❌ | 下拉值来自 `summary.consultantOptions` | 当前负责定制师精确筛 | +| plannerName | Query | String | ❌ | - | 定制师姓名模糊搜索(兼容参数,优先用 `consultantId`) | +| consultantName | Query | String | ❌ | - | `plannerName` 的别名 | +| variant | Query | String | ❌ | `list`(默认)/ `grid`;其余值返 `100001` | 视图切换 | +| page | Query | Integer | ❌ | ≥1,默认 1 | 页码 | +| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 每页条数 | +| pageNo | Query | Integer | ❌ | 同 `page` | 历史别名,映射到 `page` | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.records | Array | 需求卡片数组,结构见下方各行(本次无字段增删) | +| data.total | Long | 命中总条数(按 `orderKind` 过滤后的口径) | +| data.page | Integer | 当前页码 | +| data.pageSize | Integer | 每页条数 | +| data.records[].id | String | 与 `orderNo` 同值,列表行 key | +| data.records[].orderNo | String | 订单号 | +| data.records[].teamNo | String | 团号;order-v3 不可达时回退派车行快照 | +| data.records[].groupBatchId | String | **判定 `orderKind` 的那个值**:运营团期 ID,雪花序列化为字符串;`null` = 该订单当前不属于任何团期(含非团订单、已退团)。`orderKind=GROUP` 等价于本字段非空 | +| data.records[].orderId | String | 订单 ID(雪花字符串) | +| data.records[].requirementId | String | 用车需求 ID(雪花字符串),卡片维度 | +| data.records[].assignmentId | String | 代表派车行 ID;虚拟待派条目为 `null` | +| data.records[].virtualPending | Boolean | `true` = 还没有任何派车行的待派卡片;`false` = 已有派车行 | +| data.records[].dailyAssignments | Array | 逐日派车行的身份与状态 | +| data.records[].assignmentStatus | String | 卡片状态码(`unassigned` / `holding` / `assigned` / `completed` / `canceled` / `exception`) | +| data.records[].consultantId | String | 当前负责定制师 ID(雪花字符串) | +| data.records[].customerName | String | 客户名 | +| data.records[].productName | String | 产品名 | +| data.records[].startDate | String | 出团日 `YYYY-MM-DD` | +| data.records[].endDate | String | 返程日 `YYYY-MM-DD` | + +#### 请求示例 + +```http +GET /admin/fleet/board/orders?pageSize=100&startDayFrom=2026-01-01&startDayTo=2028-12-31&orderKind=GROUP HTTP/1.1 +Host: 192.168.100.236:8080 +Authorization: Bearer + +(GET 无请求体) +``` + +#### 响应示例 + +测试服 2026-09-28 15:13 实测原文(`orderKind=GROUP`;同窗口 `ALL` 62 条、`NORMAL` 61 条,`GROUP` 就是下面这 1 条): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "records": [ + { + "id": "HL20260928143200064", + "orderNo": "HL20260928143200064", + "teamNo": "26-8345", + "groupBatchId": "2104459089860001794", + "orderId": "2104459089616732161", + "unreadMessageCount": 0, + "assignmentId": null, + "assignmentGroupId": null, + "requirementId": "2104461918343430146", + "dailySummary": { + "serviceDate": null, + "totalDailyItems": 0, + "serviceStartDate": "2026-11-12", + "serviceEndDate": "2026-11-14", + "serviceDays": 3, + "requiredVehicleType": "mpv", + "requiredVehicleTypeLabel": "商务车", + "requiredSeats": 7 + }, + "assignmentProgress": { + "totalDailyItems": 0, + "finalizedByFleet": false, + "staleFinalizedPlan": false, + "dispatchedDailyItems": 0, + "completedDailyItems": 0, + "canceledDailyItems": 0 + }, + "dailyAssignments": [], + "customerName": "刘明远", + "contactName": "刘明远", + "productName": "王骁测试团期产品", + "headcount": 3, + "adultCount": 3, + "childCount": 0, + "youngChildCount": 0, + "babyCount": 0, + "startDate": "2026-11-12", + "endDate": "2026-11-14", + "days": 3, + "pickupAt": null, + "dropoffAt": null, + "pickupSummary": { + "required": true, + "statusCode": "MISSING", + "statusLabel": "待补接客信息", + "batchCount": 0, + "readyBatchCount": 0, + "transportNos": [], + "earliestTime": null, + "latestTime": null, + "stations": [] + }, + "dropoffSummary": { + "required": true, + "statusCode": "MISSING", + "statusLabel": "待补送客信息", + "batchCount": 0, + "readyBatchCount": 0, + "transportNos": [], + "earliestTime": null, + "latestTime": null, + "stations": [] + }, + "routeSummary": "额尔古纳市 → 满洲里市", + "daysUntilDeparture": 45, + "readiness": { + "statusCode": "PARTIAL", + "statusLabel": "部分信息待补", + "itineraryStatusCode": "READY", + "itineraryStatusLabel": "行程已完整", + "itineraryDayCount": 3, + "itineraryExpectedDayCount": 3, + "missingItemCodes": ["PICKUP_TRANSFER", "DROPOFF_TRANSFER"], + "missingItemLabels": ["接客信息", "送客信息"], + "blocksAssignment": false + }, + "isHailarPickup": false, + "isHailarDropoff": false, + "consultantId": "2103791514381664257", + "plannerName": "ha_r1_cz", + "consultantName": "ha_r1_cz", + "consultantDisplayName": "ha_r1_cz", + "customerNote": "一家三口,需儿童座椅", + "vehicleAdvice": null, + "specialTags": ["儿童安全座椅"], + "requirementRemark": "全程行程用车,含额尔古纳湿地往返", + "requiredVehicles": [ + { "vehicleType": "mpv", "categoryLabel": "商务车", "seats": 7, "count": 1 } + ], + "assignmentStatus": "unassigned", + "assignmentStatusLabel": "待派车", + "baseAssignmentStatus": "unassigned", + "manualUrgent": false, + "lifecycleStageCode": "unassigned", + "lifecycleStageLabel": "待派车", + "currentStep": 2, + "availableActionCodes": ["ASSIGN", "REJECT_REQUIREMENT"], + "currentVehiclePlate": null, + "currentVehicleModel": null, + "currentVehicleSeats": null, + "currentVehicleFleet": null, + "currentVehicleFleetTeamId": null, + "currentVehicleFleetTeamName": null, + "currentVehicleFleetTeamType": null, + "currentVehicleFleetTeamSettleType": null, + "currentDriverName": null, + "currentDriverPhone": null, + "urgentBadge": null, + "canAssign": true, + "dispatchReadOnly": false, + "dispatchReadOnlyReason": null, + "canRejectRequirement": true, + "virtualPending": true, + "groupVehicleCovered": null, + "transferChangePending": null + } + ], + "total": 1, + "page": 1, + "pageSize": 100 + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +筛选命中 0 条时返回空数组 + `total=0`,不是 `null`、不报错。下面是 `orderKind=NORMAL` 在一个只有团期订单的时间窗内的实测原文: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "records": [], + "total": 0, + "page": 1, + "pageSize": 100 + }, + "traceId": null, + "success": true +} +``` + +`NORMAL` 与 `groupBatchId` 同传属于逻辑互斥,按 AND 组合走同一条空列表路径,**不报错**。 + +order-v3 整体不可达时看板不会空、也不会 500:归属判定回退到派车行的建行快照,此时 `records[].groupBatchId` 取自快照,取值边界见「六、边界行为」。 + +#### 错误响应 + +`orderKind` 非法(大小写不符或取值域外),实测原文: + +```json +{ + "code": 100001, + "message": "参数非法: orderKind 仅支持 ALL/NORMAL/GROUP,传入非法值:group", + "data": null, + "traceId": null, + "success": false +} +``` + +HTTP 状态行仍是 **200**,判据在 body 的 `code` / `success`,不要只看状态码。未登录或 token 失效同理走 body:`{"code": 401, "message": "Token 无效", "data": null, "success": false}`。 + +#### 业务边界 + +- 鉴权:`/admin/fleet/**` 是路径级**角色**门禁(`VEHICLE_MANAGER` / `SUPER_ADMIN`),不是权限点模型,前端不按权限码做按钮显隐;非车务角色调用返 body `code=403`、`message` 为「无权限访问车务管理,请切换到车务角色」。 +- `orderKind` 大小写敏感:`group`、`Group` 一律 `100001`;首尾空白会被去掉,`" GROUP "` 合法。 +- 不传与显式传 `ALL` 的响应**逐字节相同**(2026-09-28 14:32 实测比对为真)。 +- `orderKind` 与其余所有筛选条件(含 `groupBatchId`、`statuses`、日期、车型、`consultantId`、`keyword`)都是 AND 组合。 +- 归属判定的值就是 `records[].groupBatchId`:`GROUP` ⇔ 该字段非空,`NORMAL` ⇔ 该字段为空。实测 `NORMAL` 与 `GROUP` 两档把全集二分(61 + 1 = 62,交集为空),前端可以拿返回的字段自行复核。 +- 还没有派车行的虚拟待派卡片(`virtualPending=true`)同样参与 `orderKind` 过滤,它的归属只看订单上下文的实时值。 +- 分页在过滤之后:`total` 是过滤后的条数,切换 `orderKind` 后请把页码重置回 1。 + +--- + +### 2. 派单看板汇总 `GET /admin/fleet/board/summary` + +**VO**: `BoardOrderPageReqVO → BoardSummaryVO` + +#### 使用场景 + +派单看板顶部的数字卡 + 状态页签计数 + 定制师下拉数据源。与列表接口**共用同一套筛选参数**(后端是同一个入参 VO),用于给出「同一筛选范围内」的全状态分面。切订单归属页签时,这个接口要和列表接口用**同一份** `orderKind` 一起重新拉,否则会出现「列表 1 条、状态页签写着 15 条」这种对不上的界面。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderKind | Query | String | ❌ | `ALL` / `NORMAL` / `GROUP`,**大小写敏感**;其余值返 `100001` | **本次新增**。与列表接口同一取值与缺省口径(不传或空串 = `ALL`) | +| groupBatchId | Query | Long | ❌ | 雪花 ID,字符串透传 | 运营团期精确筛。**本接口同样接受并应用该参数**,与 `orderKind` AND 组合 | +| startDayFrom | Query | String | ❌ | `YYYY-MM-DD` | 日期区间起,行程区间重叠口径 | +| startDayTo | Query | String | ❌ | `YYYY-MM-DD` | 日期区间止 | +| startDate | Query | String | ❌ | `YYYY-MM-DD` | `startDayFrom` 别名 | +| endDate | Query | String | ❌ | `YYYY-MM-DD` | `startDayTo` 别名 | +| vehicleTypeKeys | Query | String[] | ❌ | `suv` / `mpv` / `bus` / `sedan` | 车型大类多选 | +| typeKeys | Query | String[] | ❌ | 同上 | 别名 | +| driverName | Query | String | ❌ | - | 司机姓名模糊搜索 | +| keyword | Query | String | ❌ | - | 统一文字搜索,口径同列表 | +| contactName | Query | String | ❌ | - | 联系人、客户名模糊搜索 | +| contactKeyword | Query | String | ❌ | - | 别名 | +| teamNo | Query | String | ❌ | - | 团号模糊搜索 | +| consultantId | Query | Long | ❌ | - | 定制师精确筛 | +| plannerName | Query | String | ❌ | - | 定制师姓名模糊搜索(兼容参数) | +| consultantName | Query | String | ❌ | - | 别名 | +| status / statuses | Query | String / String[] | ❌ | - | **汇总中被忽略**:本接口本来就是返回同一范围内的全部状态分面 | +| page / pageSize / pageNo | Query | Integer | ❌ | - | **汇总中被忽略** | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.pendingCount | Integer | 待派车数。**随 `orderKind` 收窄** | +| data.pendingUrgentCount | Integer | 待派车中临近出团的数量。**随 `orderKind` 收窄** | +| data.todayDepartCount | Integer | 今日出发数。**随 `orderKind` 收窄**——与列表同一份过滤结果统计出来,不是全局今日出发 | +| data.idleVehicleCount | Integer | 空闲车辆数。**全局物理资源指标,不随任何订单筛选变化(含 `orderKind`)** | +| data.idleDriverCount | Integer | 空闲司机数。**同样全局,不随筛选变化** | +| data.holdingTimeoutCount | Integer | 排车锁定超时数。随筛选收窄 | +| data.statusCounts | Object | 全状态计数块,键为 `unassigned` / `holding` / `assigned` / `completed` / `canceled` / `exception` / `unassignedUrgent` / `holdingUrgent`,值为 Integer。**键集合固定不随 `orderKind` 变,只有计数收窄** | +| data.statusOptions | Array | 状态页签下发项 | +| data.statusOptions[].value | String | 状态码 | +| data.statusOptions[].label | String | 状态中文名 | +| data.statusOptions[].description | String | 状态释义 | +| data.statusOptions[].count | Integer | 该状态条数。**随 `orderKind` 收窄** | +| data.statusOptions[].urgentCount | Integer | 该状态中的紧急条数。随 `orderKind` 收窄 | +| data.consultantOptions | Array | 定制师下拉项,列表接口的 `consultantId` 取值就从这里来 | +| data.consultantOptions[].value | String | 定制师管理员 ID,雪花序列化为字符串 | +| data.consultantOptions[].label | String | 定制师显示名 | + +#### 请求示例 + +```http +GET /admin/fleet/board/summary?pageSize=100&startDayFrom=2026-01-01&startDayTo=2028-12-31&orderKind=GROUP HTTP/1.1 +Host: 192.168.100.236:8080 +Authorization: Bearer + +(GET 无请求体) +``` + +#### 响应示例 + +测试服 2026-09-28 15:13 实测原文(`orderKind=GROUP`): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "pendingCount": 1, + "pendingUrgentCount": 0, + "todayDepartCount": 0, + "idleVehicleCount": 48, + "idleDriverCount": 51, + "holdingTimeoutCount": 0, + "statusCounts": { + "unassigned": 1, + "holding": 0, + "assigned": 0, + "completed": 0, + "canceled": 0, + "exception": 0, + "unassignedUrgent": 0, + "holdingUrgent": 0 + }, + "statusOptions": [ + { + "value": "unassigned", + "label": "待派车", + "description": "已提出有效用车需求,车务尚未派车", + "count": 1, + "urgentCount": 0 + }, + { + "value": "holding", + "label": "排车中", + "description": "车务已排车,待车务确认执行", + "count": 0, + "urgentCount": 0 + }, + { + "value": "assigned", + "label": "已派车", + "description": "车务已派定,行程单已生成", + "count": 0, + "urgentCount": 0 + }, + { + "value": "completed", + "label": "已完结", + "description": "用车行程已完结", + "count": 0, + "urgentCount": 0 + }, + { + "value": "canceled", + "label": "已取消", + "description": "派车需求已取消(仅未派车即取消,#6152)", + "count": 0, + "urgentCount": 0 + }, + { + "value": "exception", + "label": "异常", + "description": "已派车后再取消,待人工处置(释放资源后处置完成)", + "count": 0, + "urgentCount": 0 + } + ], + "consultantOptions": [ + { "value": "2103791514381664257", "label": "ha_r1_cz" } + ] + }, + "traceId": null, + "success": true +} +``` + +同一时刻同一窗口的三档对照读数(证明分面随 `orderKind` 收窄,而空闲车辆/司机不随): + +| `orderKind` | `pendingCount` | `statusCounts` 桶和 | `consultantOptions` 条数 | `idleVehicleCount` | `idleDriverCount` | +|---|---|---|---|---|---| +| `ALL` | 15 | 62 | 9 | 48 | 51 | +| `NORMAL` | 14 | 61 | 8 | 48 | 51 | +| `GROUP` | 1 | 1 | 1 | 48 | 51 | + +#### 空数据 / 降级响应 + +无数据时各计数返 `0` 而不是 `null`,数组返 `[]`。下面是 `orderKind=NORMAL` 在一个只有团期订单的窗口内的实测片段: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "pendingCount": 0, + "pendingUrgentCount": 0, + "todayDepartCount": 0, + "idleVehicleCount": 48, + "idleDriverCount": 51, + "holdingTimeoutCount": 0, + "statusCounts": { + "unassigned": 0, + "holding": 0, + "assigned": 0, + "completed": 0, + "canceled": 0, + "exception": 0, + "unassignedUrgent": 0, + "holdingUrgent": 0 + }, + "consultantOptions": [] + }, + "traceId": null, + "success": true +} +``` + +注意 `consultantOptions` 为 `[]` 时定制师下拉没有候选项——这是「当前筛选范围内没有任何订单」的正常结果,不是接口故障。order-v3 不可达时汇总同样返回结构完整的 200,归属判定退化为建行快照口径。 + +#### 错误响应 + +与列表接口同一套校验,实测原文: + +```json +{ + "code": 100001, + "message": "参数非法: orderKind 仅支持 ALL/NORMAL/GROUP,传入非法值:XX", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 鉴权同列表:`VEHICLE_MANAGER` / `SUPER_ADMIN` 路径级角色门禁。 +- **本接口与列表接口必须传同一份筛选参数**,包括 `orderKind`。只改一边会让数字卡与列表对不上。 +- `status` / `statuses` 与分页参数在本接口中被忽略,传了不报错也不生效。 +- `consultantOptions` 是**筛选范围内**的定制师,不是全量管理员名单:切 `orderKind` 后下拉候选会变(实测 9 / 8 / 1),已选中的 `consultantId` 可能不在新候选里,切页签时建议连带清空定制师选择。 +- `idleVehicleCount` / `idleDriverCount` 恒为全局值,切页签这两个数字不会变,不要把它们当作「当前页签下的空闲资源」展示。 +- 不传 `orderKind` 与传 `ALL` 的响应逐字节相同(2026-09-28 14:32 实测比对为真)。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写后端接受/拒绝请求的规则,以及前端相应要改的代码位置;不写 UI 视觉建议。 + +### ✅ 正确 / ❌ 错误 query 对照 + +| 场景 | query | +|------|-------| +| ✅ 全部(推荐显式写) | `?orderKind=ALL` | +| ✅ 全部(省略等价) | `?`(不带 `orderKind`) | +| ✅ 空串等价于全部 | `?orderKind=` | +| ✅ 只看常规订单 | `?orderKind=NORMAL` | +| ✅ 只看团期订单 | `?orderKind=GROUP` | +| ✅ 某一个团 | `?orderKind=GROUP&groupBatchId=2104459089860001794` | +| ⚠️ 逻辑互斥但合法 | `?orderKind=NORMAL&groupBatchId=2104459089860001794` → 200 空列表,不报错 | +| ❌ 小写 | `?orderKind=group` → 200 + `code=100001` | +| ❌ 取值域外 | `?orderKind=XX` → 200 + `code=100001` | + +### 🔴 页签改造:`orderKind` 必须进「两个接口共用」的那份参数 + +派单看板加「常规订单 / 团期订单」两个页签,分别对应 `orderKind=NORMAL` / `orderKind=GROUP`;保留「全部」时不传或传 `ALL`。落到 `src/views/fleet/board/index.vue`(`origin/v2.1`): + +- `fetchBoard()`(`:522-525`)是并发两发:`getBoardSummary(buildBoardSharedParams())` 与 `getBoardOrders(buildBoardParams())`。 +- ⇒ **`orderKind` 要加进 `buildBoardSharedParams()`(`:491-503`),不要加进 `buildBoardParams()`(`:505-515`)**。只加后者的话列表会过滤、而数字卡与状态页签计数仍是全量口径,界面上表现为「列表 1 条、页签写着 15 条」,且不报任何错。 +- `:511` 那行注释「`009 式 summary 契约未带该参,不传`」与后端事实不符:两个接口共用同一个入参 VO,`summary` **同样接受并应用 `groupBatchId`**。改造时建议把 `groupBatchId` 一并挪进 shared params,让数字卡与列表在按团筛时也同口径;注释同步订正。 +- 两个页签**共用同一套状态枚举**:`statusCounts` 的键集合与 `statusOptions` 的 `value` 集合不随 `orderKind` 变化,只有 `count` / `urgentCount` 收窄。状态页签组件不需要按订单归属拆成两套。 +- 切页签时把页码重置回 1,并考虑清空 `consultantId`(下拉候选会随 `orderKind` 变)。 + +### 🔴 跨服务同名参数:与订单列表 `orderKind` 的语义差 + +`orderKind` 这个名字是**刻意**与 order-v3 订单列表 `GET /v3/admin/order` 对齐的(两端取值域都是 `ALL` / `NORMAL` / `GROUP`),但**缺省档与组合规则不同**: + +| 维度 | 派单看板(本次两个接口) | 订单列表 `GET /v3/admin/order` | +|------|--------------------------|-------------------------------| +| 取值域 | `ALL` / `NORMAL` / `GROUP` | `ALL` / `GROUP` / `NORMAL`(同一套) | +| 不传或空串 | **`ALL`**:团期订单照常出现 | **`NORMAL`**:只看散客;例外——传了 `productType` 而 `orderKind` 未传或为空串时,不缺省 `NORMAL` | +| 与 `groupBatchId` 同传 | **AND 组合**:`NORMAL` + `groupBatchId` 返空列表,不报错 | `groupBatchId` 传入时 **`orderKind` 被强制视为 `GROUP`** | +| 非法取值 | 业务层校验,200 + `code=100001`,message 含传入的原值 | 入参正则校验,提示「orderKind 非法,可选值: ALL, GROUP, NORMAL」 | + +**这意味着**:把订单列表页的 `orderKind` 状态直接透传给看板(或反过来),「不传」这一档的含义会翻面——在订单列表是「只看散客」,在看板是「全部」。两个页面各自维护自己的筛选状态,或者在跨页跳转时**显式写死**要传的值,不要依赖缺省。 + +唯一**静默**的失效形态就在这里:把 `NORMAL` 当成「默认值」传给看板,团期订单会从列表和汇总里一起消失,没有任何报错,表现只是少了几行。传错取值反而是响的(`100001`)。 + +另外,看板页上还有一个名字相近但**维度完全不同**的参数:到期提醒读口 `GET /admin/fleet/board/expiry` 的 `kinds`,取值是 `inspect` / `vehInsure` / `license` / `driverInsure`(车辆年检、车险、驾照、司机保险),与订单归属无关,两者不要互相传值。 + +### 🔴 团期配车模块处置:菜单行已删除,模块失去唯一入口 + +后端已把「团期配车」菜单行从 `/admin/auth` 下发的菜单树里删除(PR #8474,hl-user-service 已滚测试服,2026-09-28 15:05:03,Flyway `20260928.8464` success=1)。测试服实测:`VEHICLE_MANAGER` 账号拉到的菜单树里 `group-dispatch` **零命中**,同一份里 `/fleet` 及其余 15 个 `/fleet/*` 路径(含 `/fleet/board`)仍在。 + +**这不是「一处跳转失效」,是整个模块不可达**: + +1. hl-ui 的业务路由 **100% 由菜单树动态注册**(`src/router/index.js` 的 `addDynamicRoutes`:先 `removeDynamicRoutes()`,再按后端菜单 `generateRoutes(menus)` 挂到 `Layout` 下)。 +2. 静态路由表里**没有** `/fleet/group-dispatch` 兜底:`git grep -c -- 'group-dispatch' origin/v2.1 -- src/router` 零命中;同口径阳性对照 `git grep -c -- 'routes' origin/v2.1 -- src/router` 命中 7 个文件(`index.js`、`routes.js`、`routes.spec.js`、`navigationScope.spec.js` 等),证明查的位置与命令都有效。 +3. 菜单行不下发 ⇒ 该路由不注册 ⇒ 直链 `/fleet/group-dispatch` 落 NotFound。 +4. 涉及的是 `src/views/fleet/group-dispatch/` 下 **12 个文件**:`index.vue`、`labels.js`、6 个组件(`GroupDispatchOverviewDrawer.vue`、`GroupDispatchPlanEditor.vue`、`ShareDispositionAlert.vue`、`ShareGroupEditModal.vue`、`ShareGroupHistory.vue`、`ShareGroupPanel.vue`)、4 个组件测试;加上 `src/api/fleet/group-dispatch.js`(`:11` `const BASE = '/fleet/group-dispatch'`)。 + +**要做的处置(二选一,由 mmg 定)**:把这批页面按「不再需要」清理掉,或给它们另挂一个可达入口。两种做法都有一条**必须遵守的例外**: + +> 🔴 **`src/api/fleet/group-dispatch.js` 的 `getGroupDispatchPendingBatches` 不能一起删。** +> `src/views/fleet/board/index.vue:261` 正在 import 它,用于 #7443 的「按运营团期筛选」下拉候选(`:318-345`),与本次的归属页签是两件事。连它一起删会让看板的团期下拉直接报错。 + +**后端端点没有下线,仍然可以调用**:`/admin/fleet/group-dispatch/**`(`pending-batches` / `overview` / `resource-schedule` / `reconfigure` / `confirm` / `readiness` / `share-groups` / `share-member-candidates`)全部照旧,鉴权仍是 `VEHICLE_MANAGER` / `SUPER_ADMIN` 路径级角色门禁。测试服 2026-09-28 15:11 实测:具备车务角色的账号调 `GET /admin/fleet/group-dispatch/pending-batches` 返 200;非车务角色返 body `code=403`;伪造 token 返 body `code=401`。**删的是菜单入口,不是接口能力。** + +**看板「团期订单」页签目前覆盖到哪一步,如实说清**(决定上面二选一时需要这条): + +- `orderKind=GROUP` 做的是**列表与汇总层面的归属过滤**,返回的仍是**逐条用车需求**的卡片(维度 = 当前有效用车需求,同一 `requirementId` 一条),不是团期配车那种「一团一行」的整团视图。 +- 看板上的派车动作是**逐需求派车**;团期配车的整团逐日计划差量重配(`reconfigure`)与整团定稿(`confirm`)在看板上没有对应入口。 + +--- + +## 五、数据库行为 + +本次变更的两个接口都是只读 `GET`,**零数据库写入**,不产生任何落库副作用。`orderKind` 只影响查询结果的过滤范围,不改写任何行。 + +菜单侧(PR #8474)的库变更与接口契约无关,只删 `sys_menu` 中 `/fleet/group-dispatch` 那一行菜单及其 `sys_role_menu` 授权;**权限码 `fleet:group-dispatch:view` 与它的授权原样保留**(`/admin/fleet/**` 走路径级角色门禁,不看权限码)。 + +--- + +## 六、边界行为 + +- 未登录或 token 失效 → body `code=401`、`message` 为「Token 无效」,**HTTP 状态行仍是 200**,判据在 body。 +- 非车务角色 → body `code=403`、`message` 为「无权限访问车务管理,请切换到车务角色」。 +- `orderKind` 非法 → body `code=100001`,`message` 为 `参数非法: orderKind 仅支持 ALL/NORMAL/GROUP,传入非法值:<原值>`。 +- 命中 0 条 → `records: []` + `total: 0`;汇总各计数 `0`、`consultantOptions: []`,不返 `null`、不 500。 +- **order-v3 降级(订单上下文整体不可达)时的归属口径**:判定从「订单当前归属」退化为「派车行的建行快照」,有三个可观察差异,前端把它们当成降级期的已知边界即可,不必额外处理: + - 已退团/换团的订单,其历史派车行在 `orderKind=GROUP` 下**仍会出现**(宁可多给一条历史行,也不让整块团期看板变空); + - 建行快照为空的行(该列之前的存量派车行、车务手工建的行)在 `GROUP` 下落选、在 `NORMAL` 下命中; + - 过滤按**逐条日行**的快照判,而卡片回显的 `groupBatchId` 取**代表日行**的快照,所以同一需求下若各日行的建行快照不一致,可能出现「卡片被 `GROUP` 筛出来、但卡片上的 `groupBatchId` 为空」这类回显与筛选看起来对不上的行。非降级路径不会出现——上下文可用时两处取的是同一个订单实时归属值。 +- 老数据兼容:`records[].groupBatchId` 本来就允许为空,前端原有的空值处理逻辑不用改。 +- 未传 `orderKind` 的旧前端调用行为完全不变(缺省 `ALL`)。 + +--- + +## 六.5、枚举 / 数据字典 + +### orderKind(`com.hulalv.fleet.board.support.BoardOrderKind`) + +**所属字段**: `orderKind`(两个接口共用的查询参数) | **类型**: `String` + +> 后端是常量类(不是 Java 枚举),取值用精确相等比较 ⇒ **大小写敏感**。 + +| 值 | 中文 | 说明 | +|----|------|------| +| `ALL` | 全部 | 不按订单归属过滤。**不传、传空串、传纯空白都等价于本值** | +| `NORMAL` | 常规订单 | 当前不属于任何运营团期的订单(`records[].groupBatchId` 为空),含非团订单与已退团 | +| `GROUP` | 团期订单 | 当前属于某个运营团期的订单(`records[].groupBatchId` 非空) | + +首尾空白会被去掉后再判定;其余任何取值返 `100001`。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `orderKind`(两个接口的 query) | 不存在,传了被忽略 | 可选参数,`ALL` / `NORMAL` / `GROUP`,缺省 `ALL`,非法值返 `100001` | +| 响应字段 | — | **无任何增删改**,`BoardOrderPageRespVO` 与 `BoardSummaryVO` 的字段集合与类型一字未动 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 看板列表的订单范围 | 常规订单与团期订单混在一起,只能用 `groupBatchId` 精确筛某一个团 | 可按归属粗筛两档,也可与 `groupBatchId` AND 组合 | +| 汇总各分面的口径 | 与列表同筛选口径(不含归属维度) | 同左,并把 `orderKind` 一起纳入;`idleVehicleCount` / `idleDriverCount` 仍为全局 | +| 传了无法识别的 `orderKind` | 参数不存在,被忽略,返回全量 | 返 `code=100001`,不静默放行 | +| 「团期配车」菜单入口 | 车务管理下的独立菜单页 `/fleet/group-dispatch` | 菜单行已删除,该路由不再注册;接口与鉴权、权限码原样保留 | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。不传 `orderKind` 的旧调用行为与改前完全一致(实测不传与 `ALL` 响应逐字节相同),响应结构零变化。 +- **前端是否必须同步上线**: 是。菜单行已删除,`/fleet/group-dispatch` 路由不再注册,`src/views/fleet/group-dispatch/` 需要按「四」的二选一处置;看板的 `orderKind` 页签本身是增量能力,不做也不破坏原有功能。 +- **前端 workaround 清理点**: 若页面上原先用「先拉全量再在前端按 `groupBatchId` 是否为空分组」的方式模拟常规/团期分类,可以改为直接传 `orderKind` 让后端过滤——前端分组只能对当前这一页生效,而 `total` 与汇总计数是全量口径,两者会对不上。另:`index.vue:511` 那条关于 summary 不接受 `groupBatchId` 的注释可以订正掉。 + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台车务「派单看板」的列表与汇总两个读口,以及「团期配车」菜单入口。 +- **零影响**: + - `/admin/fleet/group-dispatch/**` 团期配车的全部接口(契约、鉴权、返回结构一字未动); + - 车务看板的其他读口:到期提醒 `board/expiry`、时间线、矩阵、接送机相关端点; + - 派车、改派、取消等所有写口; + - 订单列表 `GET /v3/admin/order` 与它自己的 `orderKind`(本次未动 order-v3 任何代码); + - 小程序端全部接口; + - 权限点 `fleet:group-dispatch:view` 与它的角色授权(本次只删菜单行,权限定义原样保留); + - 历史数据:不做任何迁移,归属判定是查询期计算,不改写存量行。 + +--- + +## 八、测试环境已验证 + +测试服网关 `http://192.168.100.236:8080`,账号 `ha_r1_vm`(`VEHICLE_MANAGER`,测试专用账号)。部署读数:hl-fleet-service `dev-v3` @ `ad4e65a27`(2026-09-28 14:22:43,ok),hl-user-service `dev-v3` @ `f49ae8d40`(2026-09-28 15:05:03,ok),Flyway `20260928.8464` success=1。 + +### 第一轮 2026-09-28 14:32(窗口 `pageSize=100&startDayFrom=2026-01-01&startDayTo=2028-12-31`,库内当时只有常规订单) + +``` +GET /admin/fleet/board/orders (不带 orderKind) → 200 + total 61 ✓ +GET /admin/fleet/board/orders?orderKind=ALL → 200 + total 61,与上一条响应逐字节相同 ✓ +GET /admin/fleet/board/orders?orderKind=NORMAL → 200 + total 61 ✓ +GET /admin/fleet/board/orders?orderKind=GROUP → 200 + total 0(空数组,不报错)✓ +GET /admin/fleet/board/summary (不带 orderKind) → 200,与 orderKind=ALL 响应逐字节相同 ✓ +GET /admin/fleet/board/orders?orderKind=group → 200 + code 100001「参数非法: orderKind 仅支持 ALL/NORMAL/GROUP,传入非法值:group」✓ +GET /admin/fleet/board/orders?orderKind=XX → 200 + code 100001(同上,原值 XX)✓ +GET /admin/fleet/board/summary?orderKind=group → 200 + code 100001 ✓ +GET /admin/fleet/board/summary?orderKind=XX → 200 + code 100001 ✓ +GET /admin/fleet/board/orders (伪造 token,阴性对照) → 200 + code 401「Token 无效」✓ +``` + +### 第二轮 2026-09-28 15:13(同一窗口,库内已有 1 条团期订单派车需求) + +``` +GET /admin/fleet/board/orders?orderKind=ALL → 200 + total 62 ✓ +GET /admin/fleet/board/orders?orderKind=NORMAL → 200 + total 61,61 条的 groupBatchId 全为 null ✓ +GET /admin/fleet/board/orders?orderKind=GROUP → 200 + total 1,该条 groupBatchId = "2104459089860001794" ✓ + 62 = 61 + 1,两档把全集二分、交集为空 ✓ +GET /admin/fleet/board/summary?orderKind=ALL → pendingCount 15 / statusCounts 桶和 62 / consultantOptions 9 项 ✓ +GET /admin/fleet/board/summary?orderKind=NORMAL → pendingCount 14 / statusCounts 桶和 61 / consultantOptions 8 项 ✓ +GET /admin/fleet/board/summary?orderKind=GROUP → pendingCount 1 / statusCounts 桶和 1 / consultantOptions 1 项 ✓ + 三档 idleVehicleCount 恒 48、idleDriverCount 恒 51 ✓ +GET /admin/fleet/board/orders?orderKind=GROUP (阳性对照,与非法值同一轮)→ 200 + code 200 ✓ +``` + +### 第三轮 2026-09-28 15:10~15:11(菜单删除后的入口与接口可达性) + +``` +GET /admin/auth 菜单树(VEHICLE_MANAGER) → group-dispatch 零命中; + 同一份里 /fleet 与其余 15 个 /fleet/* 路径仍在(阳性对照)✓ +GET /admin/fleet/group-dispatch/pending-batches (VEHICLE_MANAGER)→ 200 ✓ +GET /admin/fleet/group-dispatch/pending-batches (非车务角色) → 200 + code 403「无权限访问车务管理,请切换到车务角色」✓ +GET /admin/fleet/group-dispatch/pending-batches (伪造 token) → 200 + code 401「Token 无效」✓ +``` + +验证用团期订单:`orderNo=HL20260928143200064`、`teamNo=26-8345`、`groupBatchId=2104459089860001794`、出团日 2026-11-12。 + +--- + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| — | #7443 | 看板新增 `groupBatchId` 运营团期精确筛(活体优先、降级回退建行快照) | ✅ 有效,本次的粗筛与它同源同口径 | +| **#8473** | **#8464** | 看板两个读口新增 `orderKind` 订单归属粗筛 | ✅ 最新 | +| **#8474** | **#8464** | 删除「团期配车」独立菜单行(权限点保留) | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#8464](https://git.1814.love:8443/wx/HL/issues/8464) +- 关联 PR: [wx/HL#8473](https://git.1814.love:8443/wx/HL/pulls/8473)、[wx/HL#8474](https://git.1814.love:8443/wx/HL/pulls/8474) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8464](https://git.1814.love:8443/wx/HL/issues/8464) +- **PR**: [#8473](https://git.1814.love:8443/wx/HL/pulls/8473)、[#8474](https://git.1814.love:8443/wx/HL/pulls/8474) +- **Merge commit**: [ad4e65a27](https://git.1814.love:8443/wx/HL/commit/ad4e65a27)、[f49ae8d40](https://git.1814.love:8443/wx/HL/commit/f49ae8d40) + +### 联系人 + +- **后端负责人**: @wx