比较提交
349
次代码提交
61bfe0f9a3
..
main
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
c7d7b4c61e | ||
|
|
aa11cd2610 | ||
|
|
effa929648 | ||
|
|
a6dfbd4719 | ||
|
|
dcd914c8a5 | ||
|
|
5409e499cd | ||
|
|
12d25a7f35 | ||
|
|
c55453fb89 | ||
|
|
aeb5106d90 | ||
|
|
c87e079286 | ||
|
|
90b18a1594 | ||
|
|
3bf207769a | ||
|
|
d2b34953bc | ||
|
|
dd6794e4b8 | ||
|
|
c36c051667 | ||
|
|
a0dde3e6ad | ||
|
|
ed192a98a4 | ||
|
|
fe4fa3b70b | ||
|
|
a785dcbae6 | ||
|
|
9ab4794549 | ||
|
|
f8fb2f34b7 | ||
|
|
575292337b | ||
|
|
c96526f098 | ||
|
|
0b6467b2c3 | ||
|
|
3ce7ef580a | ||
|
|
1029581523 | ||
|
|
068e6ef2d7 | ||
|
|
6005b617a7 | ||
|
|
9571ad0ceb | ||
|
|
8835683a40 | ||
|
|
060d71bb90 | ||
|
|
ed0e5bda03 | ||
|
|
22e05ddeaf | ||
|
|
d35be59c3b | ||
|
|
f8ac9edf78 | ||
|
|
053276cfdd | ||
|
|
86da96e23e | ||
|
|
b2ca08e95e | ||
|
|
e08991a588 | ||
|
|
b4f2e3da14 | ||
|
|
58a9fc7211 | ||
|
|
9537a84303 | ||
|
|
d481025d6b | ||
|
|
8b683169ea | ||
|
|
f9b49d2852 | ||
|
|
737e20824a | ||
|
|
bb6df1469a | ||
|
|
21b7836c2b | ||
|
|
8b56ce67c6 | ||
|
|
2719ef1839 | ||
|
|
0c0d1ae396 | ||
|
|
2f5013a76a | ||
|
|
944c09775e | ||
|
|
d47e68a451 | ||
|
|
194f5df83d | ||
|
|
97056b0370 | ||
|
|
441a01d6ea | ||
|
|
c200920a76 | ||
|
|
096398a6f8 | ||
|
|
cc8292ea1e | ||
|
|
93027c27eb | ||
|
|
5e25e03897 | ||
|
|
77ab7a9a67 | ||
|
|
5d34813960 | ||
|
|
b221b2317a | ||
|
|
5203c4a599 | ||
|
|
7010d52c60 | ||
|
|
5995966e1d | ||
|
|
5b3764085a | ||
|
|
a7c93679f8 | ||
|
|
11195659e1 | ||
|
|
1c59fb716a | ||
|
|
358be6ddb2 | ||
|
|
5442f80523 | ||
|
|
27ba364e60 | ||
|
|
b54834b3f5 | ||
|
|
e66936fc5a | ||
|
|
3d0a3d7757 | ||
|
|
1e71734160 | ||
|
|
e3fe98b1f2 | ||
|
|
92c9bb51c1 | ||
|
|
ccc6c90e6f | ||
|
|
d221505375 | ||
|
|
48ddf1194e | ||
|
|
26c43ee57c | ||
|
|
cb0ec7f47d | ||
|
|
b41e3df62d | ||
|
|
9fe70242a6 | ||
|
|
9ff97c8e4c | ||
|
|
cdcd07d8e5 | ||
|
|
5c13bfd8b4 | ||
|
|
4123a6a59c | ||
|
|
7e0cdffd7d | ||
|
|
fbd759f003 | ||
|
|
df02512101 | ||
|
|
a8147a3535 | ||
|
|
ba729d6623 | ||
|
|
9c6c034e6f | ||
|
|
e1c529531c | ||
|
|
8d426a4dd1 | ||
|
|
3b93fa2606 | ||
|
|
62e9df3879 | ||
|
|
bed946723f | ||
|
|
eb2dc899f2 | ||
|
|
8b8c147a1c | ||
|
|
b82d4f4e6e | ||
|
|
86c8752549 | ||
|
|
b5c37d3724 | ||
|
|
767532e2ea | ||
|
|
79aff0aa3d | ||
|
|
c98b11fa44 | ||
|
|
95fdc98cdc | ||
|
|
b458800218 | ||
|
|
fa3213d309 | ||
|
|
8f3526b357 | ||
|
|
d3b6d38f39 | ||
|
|
ca207b14dc | ||
|
|
38164c6439 | ||
|
|
5dc79ef2e6 | ||
|
|
a29c0873e3 | ||
|
|
0e8500103a | ||
|
|
be4e9e76fa | ||
|
|
6be4c04bea | ||
|
|
653936db83 | ||
|
|
514303e28e | ||
|
|
78361b8cde | ||
|
|
69c0b76bb9 | ||
|
|
903e30b4d1 | ||
|
|
7242ab4108 | ||
|
|
d25ff94370 | ||
|
|
f40c50f1bc | ||
|
|
e1ae777695 | ||
|
|
dc080e389e | ||
|
|
78eeac7a4f | ||
|
|
5933096d07 | ||
|
|
b3de329869 | ||
|
|
877d69e651 | ||
|
|
687999963b | ||
|
|
92fb65fe68 | ||
|
|
42754367de | ||
|
|
4445618696 | ||
|
|
77092f1ae4 | ||
|
|
d1ab03e7c0 | ||
|
|
3f3f5e7558 | ||
|
|
315e9dbb2e | ||
|
|
ca26be400d | ||
|
|
0f9b98f084 | ||
|
|
a3dcb89d6e | ||
|
|
febdcfe063 | ||
|
|
0491f00fa6 | ||
|
|
c2f04d7b32 | ||
|
|
a60a791132 | ||
|
|
94fb72d79f | ||
|
|
da562f36d4 | ||
|
|
487f5fb399 | ||
|
|
52548a02a1 | ||
|
|
14d84237e7 | ||
|
|
2bce6cb8ea | ||
|
|
aeedf41639 | ||
|
|
447b3294a9 | ||
|
|
1ecfa85f54 | ||
|
|
14a332f2cb | ||
|
|
8302fb0b10 | ||
|
|
147e1b34a7 | ||
|
|
c0bf66a4e8 | ||
|
|
bb58d34367 | ||
|
|
397bac4fbc | ||
|
|
734d06d7b5 | ||
|
|
6fce073bcb | ||
|
|
76f01e1162 | ||
|
|
449caae176 | ||
|
|
46f2f0b583 | ||
|
|
b00027d107 | ||
|
|
da2ea29e67 | ||
|
|
e80d159fbb | ||
|
|
9806253b4a | ||
|
|
6bb0506f37 | ||
|
|
d7bec334e5 | ||
|
|
7c7ece465c | ||
|
|
9bcfc55fb8 | ||
|
|
fb3129af24 | ||
|
|
85e97677d8 | ||
|
|
6901219b95 | ||
|
|
f3e200bc45 | ||
|
|
b224d430a4 | ||
|
|
34e2432021 | ||
|
|
e8cf4d7479 | ||
|
|
117a6815c7 | ||
|
|
52aa4a0fd0 | ||
|
|
f6bd297303 | ||
|
|
deb65b63ec | ||
|
|
8cf973dcb0 | ||
|
|
7281031d04 | ||
|
|
423be99c79 | ||
|
|
1b159386b4 | ||
|
|
b1ae9cba79 | ||
|
|
a43cb158b8 | ||
|
|
617c16d0db | ||
|
|
11f4cd3de5 | ||
|
|
50eee05213 | ||
|
|
3da18f74b1 | ||
|
|
39b18fcd3d | ||
|
|
088c02d83d | ||
|
|
052238c102 | ||
|
|
9195d0dcd4 | ||
|
|
905705d9d0 | ||
|
|
ffecb0f2d2 | ||
|
|
96b920881c | ||
|
|
2f02fbb3a3 | ||
|
|
110dc3ff89 | ||
|
|
5c3421d8cd | ||
|
|
ec2426eda7 | ||
|
|
2a419689dc | ||
|
|
c9da0f0074 | ||
|
|
b22d2e7dca | ||
|
|
e1b39076c5 | ||
|
|
0c6da55254 | ||
|
|
580c98401c | ||
|
|
c4299265d0 | ||
|
|
2154341ea5 | ||
|
|
edf65cc6f3 | ||
|
|
9eb799f00a | ||
|
|
30ddee7b6e | ||
|
|
534841bb00 | ||
|
|
cf8cec34c1 | ||
|
|
95bec8ccf7 | ||
|
|
9fdac6e2a1 | ||
|
|
17210578cb | ||
|
|
013493d614 | ||
|
|
348d17b646 | ||
|
|
74cfa39a13 | ||
|
|
3aea7c686f | ||
|
|
bcfe323b60 | ||
|
|
e488c685d5 | ||
|
|
53a1cb9419 | ||
|
|
88caa74d34 | ||
|
|
1a396aece5 | ||
|
|
1a07718bd1 | ||
|
|
f758c29816 | ||
|
|
e02541943d | ||
|
|
982c9b1f71 | ||
|
|
18e5d38b0b | ||
|
|
69f82a17a2 | ||
|
|
9521237265 | ||
|
|
d35c418300 | ||
|
|
00711c0ca8 | ||
|
|
a178e177ff | ||
|
|
f2474d927e | ||
|
|
e884bc0fb3 | ||
|
|
af22b141d1 | ||
|
|
6306d60ded | ||
|
|
e9e28563ef | ||
|
|
5d42f7a247 | ||
|
|
079c4ded20 | ||
|
|
694c1a71be | ||
|
|
43cebd84f6 | ||
|
|
d9926ebb8f | ||
|
|
60cd1b3a70 | ||
|
|
66306da80c | ||
|
|
32d20d5696 | ||
|
|
846822af4e | ||
|
|
ec85c32bec | ||
|
|
5838313fd1 | ||
|
|
dbfeb85f61 | ||
|
|
10b46812d2 | ||
|
|
20fb092ab5 | ||
|
|
db68d11057 | ||
|
|
a04624cd2c | ||
|
|
bc88bcc489 | ||
|
|
31b3e68b7a | ||
|
|
92ce44f6b4 | ||
|
|
63d29f8538 | ||
|
|
020a3959a4 | ||
|
|
e8c8047b95 | ||
|
|
ce796dbc3c | ||
|
|
055d68a514 | ||
|
|
347b8ee929 | ||
|
|
2e3b0e892c | ||
|
|
5cbd1edafa | ||
|
|
b865fab5b8 | ||
|
|
47d93b0cf3 | ||
|
|
1fe976a7a6 | ||
|
|
05adbfcbb5 | ||
|
|
2776d18f97 | ||
|
|
e28c94c18b | ||
|
|
f872d5204a | ||
|
|
e074bd0535 | ||
|
|
54cec7e45a | ||
|
|
cda5a7adea | ||
|
|
b6672e9d99 | ||
|
|
bfc1a3210b | ||
|
|
fb00989545 | ||
|
|
bfe9bb8810 | ||
|
|
473123e2b7 | ||
|
|
f529fbf3f3 | ||
|
|
52c0f17091 | ||
|
|
4bfa0002ac | ||
|
|
b1f29bc73e | ||
|
|
f82ef9a7f2 | ||
|
|
fd58923cb7 | ||
|
|
eae70b3f8c | ||
|
|
1c7ce6d83e | ||
|
|
6461ab4909 | ||
|
|
f83c613470 | ||
|
|
57d5a24686 | ||
|
|
6793778585 | ||
|
|
147afd6b5c | ||
|
|
6b60163c9c | ||
|
|
b8db115cda | ||
|
|
5cc2f7ee8b | ||
|
|
7a31cbc816 | ||
|
|
3702e9d653 | ||
|
|
07c74aafe8 | ||
|
|
9521742f03 | ||
|
|
c1440208b2 | ||
|
|
aaefbe9dba | ||
|
|
226deac4a2 | ||
|
|
40165d4e47 | ||
|
|
5951522b47 | ||
|
|
85621d5e16 | ||
|
|
8591992142 | ||
|
|
b662606a22 | ||
|
|
ebb36b140f | ||
|
|
9a6b26748c | ||
|
|
dd72fd6a09 | ||
|
|
c494ccccdd | ||
|
|
0d44436def | ||
|
|
0e8feb0b66 | ||
|
|
7c56f43ca1 | ||
|
|
d149813991 | ||
|
|
5bda64b18a | ||
|
|
4f8393a475 | ||
|
|
49f43e50d0 | ||
|
|
302bb688cd | ||
|
|
26b3f7516a | ||
|
|
9a3d6a04a6 | ||
|
|
5397036b98 | ||
|
|
3e1e5599cd | ||
|
|
7eba84761f | ||
|
|
09d3728e92 | ||
|
|
425b4c7e76 | ||
|
|
e5af5425e8 | ||
|
|
35f60460b9 | ||
|
|
85855fbe8a | ||
|
|
4a38a8d492 | ||
|
|
82f6ada707 | ||
|
|
338f6af9ed | ||
|
|
6edd653d3e | ||
|
|
1a4a16030c |
@@ -0,0 +1,278 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "8739"
|
||||
title: "小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交"
|
||||
consumer: mp
|
||||
author: wx(GIT)
|
||||
change_type: 修改接口
|
||||
backend_status: deployed
|
||||
gateway_status: not_required
|
||||
frontend_status: pending
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: ""
|
||||
updated_at: "2026-10-03"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 场景:`POST /mp/order/create` 带 `groupBatchId` 下团期单,而管理端此刻正在给该团期「改出发日」并联动订单(#8666 引入的联动)。
|
||||
- 下单会先等联动结束,最多等 5 秒。
|
||||
- 5 秒内联动结束:按改后的日期正常成单,响应比平时慢几秒。
|
||||
- 5 秒后联动仍未结束:返回 `100503`「资源被占用,请稍后重试」,不成单,可以直接重提。
|
||||
- 改前(#8666 上线后、本次之前),同一场景下小程序约 3.5 秒就收到 `100502`「请勿重复提交订单」。测试服实测:
|
||||
- 联动在 5 秒内结束时,后台其实**已经成单**;
|
||||
- 联动占用超过 5 秒时,**没有成单**。
|
||||
|
||||
也就是说,这条路径上的 `100502` 既可能是「成了」,也可能是「没成」。本次起这条路径不再返回 `100502`。
|
||||
- 小程序服务等订单服务的上限由 2 秒放宽到 10 秒,超时后也不再自动重发下单请求。
|
||||
- 超过 10 秒仍无结果时,返回 `500`「服务暂时不可用,请稍后重试」。
|
||||
- 此时订单服务端的那次创单不会因此中止,订单**可能已经生成**。
|
||||
- 不带 `groupBatchId` 的下单不等联动、不会多等。10 秒上限和「不自动重发」对它同样适用。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 创建订单 | POST | `/mp/order/create` | 错误响应语义变更 | 团期下单撞上改期联动时最多等 5s,超时返 100503,该路径不再返 100502;服务端超过 10s 返 500,不再自动重发 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 创建订单 `POST /mp/order/create`
|
||||
|
||||
**VO**: `MpCreateOrderRequest → MpOrderDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
用户在产品详情页选好档位、人数并填好联系人后提交下单。
|
||||
|
||||
- 本次只改变下单失败和变慢时返回什么。
|
||||
- 对带 `groupBatchId` 的团期单影响最大:该团期正被管理端改出发日时,下单会先等联动结束。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productId | Body | String | 是 | 不能为空;必须是数字字符串,否则返回 600002「产品ID格式错误」 | 产品 ID(对外收字符串,防止 JS 精度丢失) |
|
||||
| groupBatchId | Body | String | 否 | GROUP 产品传;null 或空串视为未指定;非数字返回 600003「团期ID格式错误」;≤0 视为未指定 | 团期 ID。有效时下单与该团期的改期联动互斥 |
|
||||
| departureDate | Body | LocalDate | 否 | `yyyy-MM-dd` | 出发日期 |
|
||||
| adultCount | Body | Integer | 否 | ≥1,缺省 1 | 成人数 |
|
||||
| childCount | Body | Integer | 否 | ≥0,缺省 0 | 儿童数 |
|
||||
| youngChildCount | Body | Integer | 否 | ≥0,缺省 0 | 小童数 |
|
||||
| babyCount | Body | Integer | 否 | ≥0,缺省 0 | 幼童数 |
|
||||
| childNeedBed | Body | Boolean | 否 | 缺省 false | 儿童是否需要床位 |
|
||||
| tierSeq | Body | Integer | 是 | ≥1 | 档位序号 |
|
||||
| roomCount | Body | Integer | 否 | 传则 ≥1 | 房间数 |
|
||||
| sharerOpenid | Body | String | 否 | ≤64;可传空串;非空时只能含字母、数字、下划线、短横线 | 分享人 OpenID |
|
||||
| customizerId | Body | Long | 否 | ≥1 | 分享人定制师 ID |
|
||||
| contactName | Body | String | 是 | 非空白,≤50 | 联系人姓名 |
|
||||
| contactPhone | Body | String | 是 | 非空白,≤20 | 联系人电话 |
|
||||
| remark | Body | String | 否 | ≤500 | 订单备注 |
|
||||
|
||||
本次请求字段零变更。上表为完整字段表,与改前一致。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | String | 订单 ID。后端为 Long,超出 JS 安全整数范围时序列化为字符串,一律按字符串处理 |
|
||||
| orderNo | String | 订单编号,格式为 `HL` + 年月日时分秒 + 3 位毫秒 |
|
||||
| groupCode | String | 4 位团号,订金支付成功时生成;未生成时该字段不出现 |
|
||||
| groupBatchId | String | 团期 ID(序列化同 orderId),只有团期订单才出现 |
|
||||
| productId | String | 产品 ID(序列化同 orderId) |
|
||||
| departureDate | LocalDate | 出发日期 |
|
||||
| returnDate | LocalDate | 返程日期 |
|
||||
|
||||
- 响应对象带 `@JsonInclude(NON_NULL)`:值为空的字段(如订金支付前的 `groupCode`)直接不出现,不会以 `null` 返回。前端按「字段缺失」判空。
|
||||
- 本次响应字段零变更。出参与 `GET /mp/order/{orderId}` 详情接口同构;上表只列标识字段,完整字段见既有详情接口文档。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"productId": "1900123456789000001",
|
||||
"groupBatchId": "1900123456789000002",
|
||||
"departureDate": "2027-01-27",
|
||||
"adultCount": 2,
|
||||
"childCount": 1,
|
||||
"tierSeq": 1,
|
||||
"contactName": "林晓梅",
|
||||
"contactPhone": "13900001234",
|
||||
"remark": "老人同行,希望安排低楼层"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"orderId": "1900123456789000003",
|
||||
"orderNo": "HL20270127143025001",
|
||||
"groupBatchId": "1900123456789000002",
|
||||
"productId": "1900123456789000001",
|
||||
"departureDate": "2027-01-27",
|
||||
"returnDate": "2027-02-01"
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口没有空数据形态。
|
||||
|
||||
- 订单服务等待超过 10 秒,或暂时不可达时,返回下方错误响应里的 5xx(文案「服务暂时不可用,请稍后重试」),不会返回半成品数据。
|
||||
- 上述错误响应的 HTTP 状态码都是 200,结果看 `code`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期下单撞上改期联动,等满 5 秒联动仍未结束(不成单,可以直接重提):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100503,
|
||||
"message": "资源被占用,请稍后重试",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
订单服务超过 10 秒没给出结果(订单可能已经生成,先查订单列表再决定是否重提):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "服务暂时不可用,请稍后重试",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
真的重复提交(上一次下单请求已被受理或仍在处理,不要自动重提):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100502,
|
||||
"message": "请勿重复提交订单",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **什么时候会等**:只有 `groupBatchId` 解析为正数时,下单才与该团期的改期联动互斥。不带团期的下单不等。
|
||||
- **等多久**:最多 5 秒,后端常量,前端不可调整。测试服实测一次改期联动占用 41–688 毫秒(12 次),所以多数情况是「慢一点但成单」,`100503` 只在联动占用超过 5 秒时出现。
|
||||
- **`100503`**:不成单,因为等锁失败时创单业务还没开始执行。可以直接重提,不需要任何清理。
|
||||
- 重提不会撞 `100502`。小程序层的防重窗口 5 秒,`100503` 返回时已过;订单服务层的防重键在 `100503` 时随失败释放。
|
||||
- **`500` 等 5xx(等待超过 10 秒或订单服务不可达)**:小程序服务不再自动重发,但订单服务端已开始的那次创单不会中止,**可能已经成单**。
|
||||
- 此时直接重提可能产生两张订单:订单服务层同用户同产品的防重窗口是 10 秒,从原请求开始计时,到 `500` 返回时已基本过期。(按代码机制推导,测试服未构造过超过 10 秒的场景。)
|
||||
- 建议提示「网络繁忙,请到我的订单查看」,先刷新订单列表,确认没有新订单再让用户重提。
|
||||
- **`100502` 现在只表示真的重复提交**,有两层窗口:
|
||||
- 小程序层:同一用户 5 秒内第二次下单,不分产品。成功下单后 5 秒内再下也算。
|
||||
- 订单服务层:同一用户同一产品 10 秒内第二次下单。
|
||||
|
||||
收到时说明前一次请求已被受理或仍在处理,引导用户到订单列表查看,不要自动重提。
|
||||
- **`100501`「下单过于频繁,请稍后再试」**:订单服务层按用户限流,每个用户 60 秒内最多 5 次下单请求。既有行为,未变。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
1. 按 `code` 区分错误,不要按 `message` 判断。`100502`、`100503` 和 5xx 的文案相近,处理方式完全不同。
|
||||
2. 下单请求的前端超时不要短于 15 秒:服务端最长 10 秒给出结果,另有网关与网络开销。前端先放弃时,用户看不到 `100503`,后台却可能已成单。
|
||||
3. 提交期间禁用提交按钮,直到收到响应。团期下单撞上改期联动时,正常响应也可能慢约 5 秒。
|
||||
4. 收到 `100503` 可以直接重提原请求。
|
||||
5. 收到 5xx(`code` ≥ 500)先刷新订单列表,确认没成单再重提。
|
||||
6. 收到 `100502` 不要自动重提,引导用户到订单列表查看。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 结果 | 数据库写入 |
|
||||
|------|------------|
|
||||
| `100503` | 零写入:等锁失败发生在创单业务开始之前 |
|
||||
| `100502` / `100501` | 零写入:在进入创单业务前就被拦截 |
|
||||
| 5xx(等待超过 10 秒) | 订单服务端那次创单照常提交或失败,与小程序有没有收到结果无关 |
|
||||
| 成功 | 写入与改前完全一致;本次不涉及任何表结构变更 |
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 两个时限(等锁 5 秒、小程序服务调订单服务超时 10 秒)都是服务端固定值,请求参数无法调整。
|
||||
- 本接口与管理端创单复用同一套创单内核,锁判断完全一致。
|
||||
- 管理端用 `productBatchId` 数字。
|
||||
- 小程序用 `groupBatchId` 字符串,两者是同一个 ID。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
本接口请求、响应字段均**零变更**,变的只是失败和变慢时的返回。
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 场景 | 改前(#8666 上线后、本次之前) | 改后 |
|
||||
|------|------|------|
|
||||
| 团期下单,没撞上改期联动 | 正常成单 | 正常成单,无差异 |
|
||||
| 团期下单,撞上改期联动且联动在 5 秒内结束 | 约 3.5 秒返回 `100502`,后台其实已成单 | 等联动结束后正常成单,响应最多慢约 5 秒 |
|
||||
| 团期下单,联动占用超过 5 秒 | 约 3.5 秒返回 `100502`,没有成单 | 约 5 秒返回 `100503`,没有成单,可直接重提 |
|
||||
| 任意下单,订单服务处理超过 2 秒 | 小程序服务约 2 秒超时后自动重发同一请求,用户可能收到 `100502` 而后台已成单 | 等到 10 秒;超过 10 秒返回 `500`,不重发 |
|
||||
|
||||
在 #8666 之前,下单完全不与改期联动互斥。撞上改期时可能按旧出发日成单,且日期与团期永久错位、无人察觉。#8666 堵住了这个窗口,本次修正的是它在小程序链路上的错误码表现。
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否。请求、响应字段零变更,正常成单路径不变。
|
||||
- **前端是否必须同步上线**:否。未识别 `100503` 时展示原始 `message` 不会白屏。但若前端下单请求的超时短于 15 秒,需要调整,见第四节第 2 条。
|
||||
- **前端 workaround 清理点**:无。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 小程序其他订单接口(列表、详情、取消、改单等)调订单服务的等待上限与重试策略**未变**。
|
||||
- 请求、响应字段零变更。
|
||||
- 网关路由零变更,无数据库结构变更。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
2026-10-03 测试服部署 hl-mp-service(dev-v3 `f4e45c2f1`)后实测。做法:用测试 C 端用户直连小程序服务,在自建团期上人为占住改期联动锁。
|
||||
|
||||
| 场景 | 响应 | 耗时 | 成单 | 订单服务收到的请求 |
|
||||
|------|------|------|------|------|
|
||||
| 联动占用约 3.5 秒 | 成功,`data` 为完整订单详情 | 约 3.5 秒 | 1 单 | 1 次 |
|
||||
| 联动占用约 6 秒 | `100503`「资源被占用,请稍后重试」 | 约 5 秒 | 0 | 1 次 |
|
||||
| 上一行锁释放后间隔 ≥5 秒重提 | 成功 | 正常 | 1 单 | 1 次 |
|
||||
| 收到 `100503` 后 0.3 秒内立即重提(锁剩余不足 1 秒) | 未返回 `100502`,等联动结束后成功 | 未单独计时 | 1 单 | 2 次,即两次真实提交,无自动重发 |
|
||||
| 不占锁的常规下单 | 成功 | 正常 | 1 单 | 1 次 |
|
||||
|
||||
- 常规下单的响应 `data` 与 `GET /mp/order/{orderId}` 的 `data` 做了字段集比对:61 个字段路径,零差异,二者同为 `MpOrderDetailVO`。
|
||||
- 改前基线是部署前用同一夹具、同一方法测的:
|
||||
- 联动占用约 3.5 秒:约 3.5 秒返回 `100502`,但后台已成单,订单服务收到 2 次。
|
||||
- 联动占用约 6 秒:同样返回 `100502`,未成单。
|
||||
- 本轮未构造订单服务超过 10 秒才返回的场景,所以 `500` 之后可能已成单这一点仍是按代码推导,见业务边界。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 管理端同源改动:`changelogs-v2/2026-10/03_8666_产品班期改出发日联动团期与子单日期并新增改期拒绝码-修改接口-管理后台.md`(#8666)。改期联动本身的事件与拒绝码以该条为准。
|
||||
- 内部转发路径:`POST /mp/order/create`(hl-mp-service)→ `POST /v3/internal/mp/order/create`(order-v3)。`/v3/internal/**` 只供服务间调用,不经网关,前端不可达。
|
||||
- `100503` 是全仓统一的锁竞争可重试错误码。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8739](https://git.1814.love/wx/HL/issues/8739)(本次)、[#8666](https://git.1814.love/wx/HL/issues/8666)(引入改期联动锁)
|
||||
- **PR**: [#8740](https://git.1814.love/wx/HL/pulls/8740)、[#8738](https://git.1814.love/wx/HL/pulls/8738)
|
||||
- **Merge commit**: `f4e45c2f147eb5e0deec4dbd920e6c82c496ec7b`(#8740)、`fad5d7814b2d1fb940d740457dbed754e46d03f9`(#8738)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5186"
|
||||
title: "排车中订单恢复派车派人入口并补齐改派上下文"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "158f728ce9110ca6a9e0185c137969f84b522c6f"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-23 交付(恢复排车中订单派车派人入口,158f728ce 起含 2f4d278a6/54c007856 后续修正),证据为交付 commit 正文标注 #5186"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-23T16:10:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5194"
|
||||
title: "待确认详情补全多车多司机与按槽位改派"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "bfb4e3cde018d14b2b5dedaacacb83128cc7714d"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-23 交付(多槽位改派与司机确认,bfb4e3cde),证据为交付 commit 正文标注 #5194"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-23T18:02:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5199"
|
||||
title: "车务首页订单去重与字段补全"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "3c4f8619bbc569d72e73cedba9a74b2cefc67402"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-24 交付(车务首页订单聚合契约对齐,3c4f8619b),证据为交付 commit 正文标注 #5199"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-24T09:46:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5200"
|
||||
title: "用车需求驳回历史与重新提交"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "d7afc25146ed1bceea3e3722c521babd85c1345e"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-24 交付(用车需求驳回历史与重新提交,d7afc2514),证据为交付 commit 正文标注 #5200"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-24T11:05:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5205"
|
||||
title: "车务首页汇总与看板人员类型展示"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "78cbb1565b6555788607b34f591f0291a87af96d"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-24 交付(车务汇总与人员类型展示对齐,78cbb1565),证据为交付 commit 正文标注 #5205"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-24T10:45:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5132"
|
||||
title: "车务派单详情补全产品、行程节点、出行人与大交通"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "524cfb21d24074e8e22c5ca7a89cbbd693f59abb"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(派单详情新增字段展示/出行人类型区分,524cfb21d 起 3 个 commit),证据为交付 commit 正文标注 #5132"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T10:35:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5139"
|
||||
title: "车务派单候选筛选、分页与任意车辆选择"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "a0e8a6ef15a5a1983ddab3ca2958444e7971ae77"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(派单候选筛选与任意车辆选择接入,a0e8a6ef1),证据为交付 commit 正文标注 #5139"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T13:25:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5141"
|
||||
title: "车务派单司机保险类型与行程保障状态"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "29ab0d6562413be6ab715d8b1e1eb2d4521a9eff"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(司机保险保障状态与线上投保接入,29ab0d656),证据为交付 commit 正文标注 #5141"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T14:07:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5145"
|
||||
title: "选车后常驻司机默认配对"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "b9994da9308ae84d361e79e86f74200813ec06cc"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(选车后常驻司机默认配对接入,b9994da93),证据为交付 commit 正文标注 #5145"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T15:30:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5146"
|
||||
title: "司机待确认通知与可选确认凭证"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "089e55ffb6f4336c19ffd2ed2eade131bdb35bf7"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(司机确认登记与可选凭证接入,089e55ffb),证据为交付 commit 正文标注 #5146"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T15:20:00+08:00"
|
||||
---
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
---
|
||||
schema: "hl-changelog/v1"
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5149"
|
||||
title: "派单详情补充团号与产品类型"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
change_type: "修改接口"
|
||||
backend_status: "verified"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-admin(claude)"
|
||||
frontend_ref: "ccbdd0bd1b7c903846c8ae9e4e9e15d80266b9a4"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-10-09"
|
||||
status_note: "7 月历史补标:已于 2026-07-22 交付(派单详情补充团号与产品类型,ccbdd0bd1),证据为交付 commit 正文标注 #5149"
|
||||
updated_at: "2026-10-09"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-22T16:22:00+08:00"
|
||||
---
|
||||
|
||||
@@ -10,10 +10,10 @@ gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "af7cc62b"
|
||||
target_release: ""
|
||||
target_release: "hl-ui@af7cc62b"
|
||||
verified_at: "2026-09-08"
|
||||
status_note: "后端 PR #7170 已合 dev-v3 并部署测试服(网关 finance/advances/payee-candidates 实测 200),原 backend_status/gateway_status=pending 系 09-06 陈旧快照已修正为 deployed/verified。前端团期财务 Tab 与预支接真已交付:groupBatchFinance.js 五端点+FinanceTab(四金额卡/逐户付款表/预支记录/整团核算口径行)+GroupAdvanceModal+detail 挂财务 Tab+store 键缓存+AdvanceApprovalList scope 筛选;金额全后端权威值零反算。ref af7cc62b,checkpoint 全绿含 Vitest 全量+生产构建。"
|
||||
updated_at: "2026-09-08"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
@@ -378,8 +378,8 @@ POST /v3/admin/order/group-batch/90211/advance
|
||||
|---|---|
|
||||
| `589500` | 团期不存在 |
|
||||
| `589507` | 缺 `group-batch:finance:advance` 权限 |
|
||||
| `589538` | 团期状态不可发起预支(须为物料准备中 / 待出发 / 出行中) |
|
||||
| `589539` | 房 / 车 / 导 / 摄四项未配齐 |
|
||||
| `589541` | 团期状态不可发起预支。⚠️ 2026-09-24 订正:原文误写为 `589538`(号段顺延后实际落 589541);允许状态已两次放宽,现为「进入核单前都可以」(#8270 / #8322) |
|
||||
| `589542` | 房 / 车 / 导 / 摄四项未配齐。⚠️ 2026-09-24 订正:原文误写为 `589539`(该号实为 #7178「名额调整量不能为 0」);**#8270 起本码不再返回** |
|
||||
| `585003` | 预支金额必须大于 0 |
|
||||
| `585004` | 预支金额超过可用余额上限 |
|
||||
| `585006` | 借款类型非法 |
|
||||
@@ -391,7 +391,7 @@ POST /v3/admin/order/group-batch/90211/advance
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **双前置闸门**:团期状态 + 四项资源全就绪,缺一即拒(此前服务端两道都没有,只有前端做了置灰)。
|
||||
- **双前置闸门**:团期状态 + 四项资源全就绪,缺一即拒(此前服务端两道都没有,只有前端做了置灰)。⚠️ 已变更:#8270 删除四项配齐门,#8322 起进入核单前都可发起,且领款人限定为本团主报账人(589557),见 `24_8322_…` 条目。
|
||||
- **上限走团期统一池**:团期级与各子订单级**共扣一池**。团期把整团尾款预支满后,该团任一子订单再发起订单级预支同样会被 `585004` 拒——这是本次修复的超支漏洞。
|
||||
- 创建后进入**站内财务审批**,走既有 `advance-approvals` 列表与 approve / reject / 撤回三端点,与订单级完全一致。
|
||||
- 一期仍**只记账不出款**,实际出款走财务既有付款流程。
|
||||
@@ -583,8 +583,8 @@ GET /v3/admin/order/group-batch/90211/settlement/summary
|
||||
| 团期无活跃子订单 | 六个金额字段 `0.00`,`items` 空数组 |
|
||||
| 子订单已取消 | 不进金额、不进 `items`,只计入 `withdrawnCount` |
|
||||
| 未设置报账人 | `primaryPayeeName` 为 `null`,后端不兜底默认导游 |
|
||||
| 团期状态为招募中 / 核单中 | 发起预支返回 `589538` |
|
||||
| 四项资源缺任一 | 发起预支返回 `589539` |
|
||||
| 团期状态为招募中 / 核单中 | 发起预支返回 `589541`(2026-09-24 订正码值;#8322 起招募中已可发起,仅核单中及之后返回) |
|
||||
| 四项资源缺任一 | 原返回 `589542`(2026-09-24 订正码值);#8270 起不再拦截 |
|
||||
| 团期尾款池已被预支占满 | 团期级与该团任一子订单级预支**均**返回 `585004` |
|
||||
| 数据字典服务不可用 | 借款类型降级到内置集合校验,正常类型仍可提交 |
|
||||
| 审批列表出现团期级行 | `orderId` / `orderNo` / `consultantName` / 订单四态均为 `null` |
|
||||
@@ -611,7 +611,7 @@ GET /v3/admin/order/group-batch/90211/settlement/summary
|
||||
1. 财务 Tab 四张卡与逐户表末行合计一致,且与团期详情、看板列表三处应收同源。
|
||||
2. **超支被堵死**:团期级把整团尾款预支满 → 该团任一子订单再发起订单级预支被 `585004` 拒。
|
||||
3. **核单零重复扣**:团期级预支 5,000 通过 + 某户订单级预支 2,000 通过 → 该户报销单含 2,000,整团 `groupAdvanceApproved` 仍是 5,000;`grandTotalCost` 不变。
|
||||
4. 两道闸门各拒一次(`589538` / `589539`)。
|
||||
4. 两道闸门各拒一次(`589541` / `589542`,2026-09-24 订正码值)。
|
||||
5. 团期级预支出现在预支审批列表,「团号 / 产品」「行程」两列有值,按团期号搜得到,就地通过 / 驳回 / 撤回正常。
|
||||
6. 订单级预支五端点回归无变化。
|
||||
|
||||
@@ -627,7 +627,7 @@ GET /v3/admin/order/group-batch/90211/settlement/summary
|
||||
|
||||
| 处 | 文档现状 | 实际 |
|
||||
|---|---|---|
|
||||
| §0B.9 错误码 | 「统一用 `AdvanceErrorCode`(585 段)」 | 改落 `589538` / `589539`;585 段被 v2/v3 整段重叠声明且 585001-585010 已被 order-v2 实占 |
|
||||
| §0B.9 错误码 | 「统一用 `AdvanceErrorCode`(585 段)」 | 改落 `589541` / `589542`(原拟 589538 / 589539,被 #7158 / #7178 先占后顺延,2026-09-24 订正);585 段被 v2/v3 整段重叠声明且 585001-585010 已被 order-v2 实占 |
|
||||
| GB-ADM-040 出参 | `payStatus` 写五值含 `REFUNDING` / `REFUNDED` | 代码只有三值;结清状态另出 `settleStatus` 字段 |
|
||||
| GB-ADM-040 出参 | 含 `advanceTotal` | 已废,改三个数 |
|
||||
| GB-ADM-042 | 返回 `GroupBatchWriteResultVO`、不回传 `advanceId` | 改返 `OrderAdvanceRespVO` 并回传 `advanceId` |
|
||||
|
||||
@@ -362,6 +362,8 @@ POST /v3/admin/order/60123456789013/vehicle-requirement/supplier-reject?kind=TRA
|
||||
|
||||
### 5. 我的接单列表 `GET /v3/admin/order/grab-pool/my-claims/vehicle`
|
||||
|
||||
> 2026-09-26 订正(#8373):本接口已下线,见 26_8373_下线车控我的接单接口-删除接口-管理后台.md
|
||||
|
||||
**VO**: `MyClaimsQueryReqVO → Result<PageResult<VehicleClaimItemVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
+2
-2
@@ -7,12 +7,12 @@ author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "订正件,不改代码,只改测试服 Nacos 配置 + 重启。18_7443 判 not_required 的理由是「809009 开关关闭,产品上产不出 TRANSFER 需求,4 个新错误码前端不可达」——该理由 2026-09-19 15:33:32 起已不成立:hl.order.requirement.transfer-kind-submit-enabled 在测试服 namespace=test 的 hl-order-service-v3-test.yml 被置 true 并随 order-v3 两实例重启(15:36:32/15:36:46)生效。本文撰写前二次复验(Nacos 直读 17:00:35 CST 仍为 true;SUPER_ADMIN 身份两次真实网关调用穿透了原先必现的 809009 分支,其中一次落库成功)。三条边界必须同时看:①代码默认值仍是 false(@Value 未改),生产环境未动;②RequirementService 无 @RefreshScope,改配置必须重启才生效,这次也确实重启了;③本文撰写过程中意外发现 18_7443/19_7443(未推送草稿) 关于「confirm/change 仍必现 605041」的既有说法本身已经过期——AC-24 修复(PR #7952, commit c527e48c3, 2026-09-18 18:55 合入 dev-v3)已经是当前测试服 order-v3(ca0656f1c)与 fleet(31fd5b5e6) 两个正在跑的字节的祖先提交,即该修复本身也已随各自服务的最近一次部署上线,但本文未对 confirm/change 端点做真实网关复测,这一句只有源码祖先关系与部署记录支撑,不构成端到端功能验证,请勿据此对外宣称\"确认/改派已验证可用\"。frontend_status 由 not_required 改判 pending:原判定的前提(不可达)已不成立,但生产环境仍不可达、且是否现在启动前端集成属产品排期决定,不是本文档能替 mmg 拍板的,故不写 not_required(意味着无需关注),也不写 claimed/implemented(没有证据),交给 mmg 自行判断是否现在启动。"
|
||||
status_note: "订正件,不改代码,只改测试服 Nacos 配置 + 重启。18_7443 判 not_required 的理由是「809009 开关关闭,产品上产不出 TRANSFER 需求,4 个新错误码前端不可达」——该理由 2026-09-19 15:33:32 起已不成立:hl.order.requirement.transfer-kind-submit-enabled 在测试服 namespace=test 的 hl-order-service-v3-test.yml 被置 true 并随 order-v3 两实例重启(15:36:32/15:36:46)生效。本文撰写前二次复验(Nacos 直读 17:00:35 CST 仍为 true;SUPER_ADMIN 身份两次真实网关调用穿透了原先必现的 809009 分支,其中一次落库成功)。三条边界必须同时看:①代码默认值仍是 false(@Value 未改),生产环境未动;②RequirementService 无 @RefreshScope,改配置必须重启才生效,这次也确实重启了;③本文撰写过程中意外发现 18_7443/19_7443(未推送草稿) 关于「confirm/change 仍必现 605041」的既有说法本身已经过期——AC-24 修复(PR #7952, commit c527e48c3, 2026-09-18 18:55 合入 dev-v3)已经是当前测试服 order-v3(ca0656f1c)与 fleet(31fd5b5e6) 两个正在跑的字节的祖先提交,即该修复本身也已随各自服务的最近一次部署上线,但本文未对 confirm/change 端点做真实网关复测,这一句只有源码祖先关系与部署记录支撑,不构成端到端功能验证,请勿据此对外宣称\"确认/改派已验证可用\"。frontend_status 由 not_required 改判 pending:原判定的前提(不可达)已不成立,但生产环境仍不可达、且是否现在启动前端集成属产品排期决定,不是本文档能替 mmg 拍板的,故不写 not_required(意味着无需关注),也不写 claimed/implemented(没有证据),交给 mmg 自行判断是否现在启动。 【mmg 2026-09-24 复核,维持 not_required】四码(602200/602201/602202/602205)前端处理已随 20_7443(09-21,hl-admin 662310ea)交付:batch/pickup-dropoff-config 显式 kind=TRANSFER(仅切换器标记者,TRAVEL 绝不下发),602205 透「先补大交通再重试」引导,602200-602202 透 message(board.js:91/useAssignFlow.js:366,891);确认/改派四端点契约零变化。遗留:TRANSFER 复核确认(待后端派车行 kind 标签)、我的接单 vehicle 分栏(前端无该页面),非本件动作。"
|
||||
updated_at: "2026-09-19"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
@@ -10,10 +10,10 @@ gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "28d4482c636dbfeba1803c64ef263f09c3656934"
|
||||
target_release: ""
|
||||
target_release: "hl-ui@28d4482c"
|
||||
verified_at: "2026-09-21"
|
||||
status_note: "前端 2026-09-20 反馈清单第4项引用了一个不存在的写口 POST .../slots/{slotId}/clear-residue-dates(该端点已随 #7067 去槽位化整体删除,origin/dev-v3 全仓零命中),并要求后端在读侧新增 residueServiceDates 字段。核实:读侧 cells[].rescheduleResidue 已能推导出该日期子集,写侧 DELETE /{assignmentId} 与 POST /batch 两条既有端点已覆盖单日/整批清理,本次零后端代码改动。POST /batch 对已过去的残留日期有硬门禁会拦截,DELETE 是否放行未取证,本文档已如实标注【需确认】。前端 2026-09-21 已交付:Step2 逐日栅格恢复残留格「取消残留」入口(canDelete!==false 且 assignmentId 时渲染,只读行禁用,deleteBlockReason 透传),逐日清理走 DELETE /admin/fleet/assignments/{assignmentId}(cancelReason/driverNotified/requestId 按契约显式传,requestId=createRequestId('residue-clear'));整批 POST /batch 100001「已过去或已完结」识别后提示改走逐日清理。DELETE 对过去日期是否放行(605028)仍未实证,由拦截器统一透后端 message 兜底。"
|
||||
updated_at: "2026-09-20"
|
||||
status_note: "前端 2026-09-20 反馈清单第4项引用了一个不存在的写口 POST .../slots/{slotId}/clear-residue-dates(该端点已随 #7067 去槽位化整体删除,origin/dev-v3 全仓零命中),并要求后端在读侧新增 residueServiceDates 字段。核实:读侧 cells[].rescheduleResidue 已能推导出该日期子集,写侧 DELETE /{assignmentId} 与 POST /batch 两条既有端点已覆盖单日/整批清理,本次零后端代码改动。POST /batch 对已过去的残留日期有硬门禁会拦截,DELETE 是否放行未取证,本文档已如实标注【需确认】。前端 2026-09-21 已交付:Step2 逐日栅格恢复残留格「取消残留」入口(canDelete!==false 且 assignmentId 时渲染,只读行禁用,deleteBlockReason 透传),逐日清理走 DELETE /admin/fleet/assignments/{assignmentId}(cancelReason/driverNotified/requestId 按契约显式传,requestId=createRequestId('residue-clear'));整批 POST /batch 100001「已过去或已完结」识别后提示改走逐日清理。DELETE 对过去日期是否放行(605028)仍未实证,由拦截器统一透后端 message 兜底。2026-09-26 订正(#8371):DELETE 命中改期残留行已改为单行取消,并豁免 605027/605047/605028 三道日期门,见 26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
@@ -41,6 +41,10 @@ base: "dev-v3"
|
||||
- 前端要求新增的读侧字段 `residueServiceDates`**不需要新增**:`AssignmentCandidateRespVO.CanonicalSnapshotVO.cells[]`(`AssignmentCandidateRespVO.java:108-150`)里每个 cell 已经逐日携带 `rescheduleResidue`(布尔)+ `serviceDate` + `assignmentId`,前端自己过滤即可得到这个数组。
|
||||
- ⚠️ **`POST /batch` 整批清理对"已过去的残留日期"有硬门禁会拦截**(详见「四、契约约束」),前端做整批清理时必须预期这种场景下的 400。逐日清理 `DELETE /{assignmentId}` 是否放行过去日期,本轮**未取证**,标记为【需确认】。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:
|
||||
> - `DELETE /{assignmentId}` 逐日清理残留行现已**单行取消**(不再整组取消),并豁免 605027/605047/605028 三道日期门。
|
||||
> - 改期残留行判定、单行处理、错误码豁免的细节,见 26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
@@ -72,6 +76,8 @@ base: "dev-v3"
|
||||
| 2 | 取消派单(逐日清理残留) | DELETE | `/admin/fleet/assignments/{assignmentId}` | 复用不改 | 按 cell 的 `assignmentId` 单日取消,条件释放占用 |
|
||||
| 3 | 批量创建派单(整批清理残留) | POST | `/admin/fleet/assignments/batch` | 复用不改 | 提交时不带残留日期项,后端按 diff 精确取消 |
|
||||
|
||||
> **2026-09-26 订正(#8371)**:上表第 2 行「按 cell 的 `assignmentId` 单日取消」在写作当时并不成立——彼时 `DELETE` 命中改期残留行仍按**整组**取消(还会连带取消同组窗内的其它日期行,这正是 #8371 要修的缺陷)。#8371 已实测确认:`DELETE` 现在对「改期残留行」做真正的单行取消,窗内其它行不受影响;非残留行仍按原整组逻辑处理。细节与实测证据见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
@@ -197,6 +203,8 @@ base: "dev-v3"
|
||||
|
||||
**VO**: `CancelReqVO → CancelRespVO`
|
||||
|
||||
> **2026-09-26 订正(#8371)**:本小节标题「逐日清理残留」写作时是前端诉求的期望描述,当时源码并不支持——`DELETE` 命中改期残留行时仍按整组取消。#8371 已实测确认标题所述行为现已成立:目标行若判定为「改期残留行」,走单行取消分支,操作日志 `operationLog[].detailJson` 会带 `scope="RESIDUE_ROW"` 可供核验;非残留行不受影响,仍是整组取消。细节见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务在 Step2 栅格里勾选一个或多个 `rescheduleResidue=true` 的日期后,对每个勾选日期取其 cell 的 `assignmentId`,逐日调用本端点单独取消,条件释放该行占用的车辆/司机。适合「只清理某几天」「部分勾选」场景。
|
||||
@@ -287,9 +295,13 @@ base: "dev-v3"
|
||||
|
||||
其它错误码(`AssignmentController.java:469-471`):400(`cancelReason`/`driverNotified`/`requestId` 请求校验)/ 605009(派单不存在)/ 605020(当前状态不允许取消)/ 605026(无凭证未二次确认)/ 605027(已出发禁止整组取消)。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:目标行判定为「改期残留行」时,605027(连同 605047/605028)**不再触发**,改走单行取消;新增 605710「订单服务不可用:{0}」——残留行判定依赖 order-v3 查询窗口数据,查询失败时整笔 fail-closed,不取消任何行。非残留行的错误码集合不变。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 【需确认】**本端点对"服务日期已过去的改期残留行"是否放行取消,本轮未取证**:`CancelReqVO.cutoffDate` 注释写明「服务端仅接受当天,空值按当天处理,不支持预约未来取消」,而 605028 的语义是「取消生效日不在派单服务日期范围内」——若某残留行 `serviceDate` 是 3 天前、`cutoffDate` 按当天处理,"当天"是否落在该行的服务日期范围内、会不会触发 605028,未经实测确认。读侧 `canDelete` 字段的设计意图是「改期残留旧行恒 `true`,受只读窗口约束时 `false`」(`AssignmentCandidateRespVO.java:149-150`),但这是响应快照上的展示态,不等于运行时 DELETE 调用本身在过去日期上必然放行。**若实测发现本端点对已过去日期同样拦截,那才是真正需要后端开一张单的地方**(放开残留行对过去日期的取消)。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:已实测坐实——修复前,改期残留行(服务日期已过去)确实被拦截,实测复现 605027;#8371 修复后,同一残留行 `DELETE` 直接取消成功,不再命中 605027/605047/605028。取消后该行状态落 `exception`(不是 `canceled`,因取消前处于 `assigned`/`holding`,与通用取消 #6152 同口径)。判定逻辑与全链路证据见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
- `assignmentId` 必须取自对应 cell(残留清理场景下即 `rescheduleResidue=true` 的那一项),传错会命中另一天的派车行。
|
||||
- `driverNotified` 传 `false` 也允许取消,只是前端需要在未告知司机时给出强提示(后端只做存证,不代为通知)。
|
||||
|
||||
@@ -416,6 +428,8 @@ base: "dev-v3"
|
||||
|
||||
前端在实现"整批清理"按钮时,遇到 `code=100001` 且消息含"已过去或已完结",应识别为"残留日期已过去,本接口无法清理",引导车务改走逐日 `DELETE` 路径(而不是当成普通校验失败重试原样提交)。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:上表「✅ 逐日清理(含过去日期,未取证是否放行)」与本段「引导车务改走逐日 `DELETE`」在写作时均未经实测;实测结果是**当时同样会被拦截**(改期残留行过去日期 `DELETE` 命中 605027),引导车务改走逐日 `DELETE` 在当时并不能真正解决问题。#8371 已修复:残留行现在 `DELETE` 直接取消成功,完全跳过整组门禁,上表与本段的方向自此才成立。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
@@ -429,6 +443,8 @@ base: "dev-v3"
|
||||
|
||||
两条路径都不会物理删除行,只做状态流转;`assignmentId` 一旦取消不可复用为新方案的匹配键。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:上表「状态置 `canceled`」是简化写法。测试服实测:取消前处于 `assigned`/`holding` 的行,落库状态是 `exception`(与通用取消 #6152 同口径),仅 `unassigned` 行取消才直接落 `canceled`。改期残留行走单行取消分支时同样遵循这条状态机,不因残留身份而特殊化。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
@@ -437,6 +453,8 @@ base: "dev-v3"
|
||||
- `assignmentId` 不存在 → `DELETE` 返 605009
|
||||
- `requirementId` 缺失或需求上下文不可用 → `candidates` 的 `canonicalSnapshot` 为 `null`,不报错
|
||||
- 已出发/已完结的派单 → 605027(整组取消禁止)/「该日已完结不可改派」400(`AssignmentService.java:2714`)
|
||||
|
||||
> **2026-09-26 订正(#8371)**:上条仅对**非改期残留行**成立。目标行判定为改期残留行时,605027/605047/605028 三道日期门全部豁免,按单行取消处理;order-v3 查询窗口失败时新增 605710 fail-closed(不取消任何行)。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
- 老数据兼容:历史行无 `assignmentGroupId` 时,`cells[].groupId` 回退返回 `assignmentId`,对任何真实行恒非空
|
||||
|
||||
---
|
||||
@@ -498,6 +516,8 @@ grep clear-residue-dates / clearResidueDates / cancel-residue → 全仓零命
|
||||
|
||||
**【需确认】未取证项**:`DELETE /{assignmentId}` 对服务日期已过去的改期残留行是否放行取消(涉及 605028 与只读窗口判定),需要一次真实测试服调用才能确认,本文档不代为下结论。
|
||||
|
||||
> **2026-09-26 订正(#8371)**:已取证,不再是【需确认】。改前基线:确实拦截,命中 605027。改后(#8371 测试服全链路 6 步实测):改期残留行判定为真时,`DELETE` 直接放行取消,不命中 605027/605047/605028;判定失败(order-v3 不可达)时新码 605710 fail-closed。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
@@ -506,6 +526,8 @@ grep clear-residue-dates / clearResidueDates / cancel-residue → 全仓零命
|
||||
- 退役背景:#7067 去槽位化重构(`06_7067_派单去槽位化按行程日配车-接送机独立配置-修改接口-管理后台.md`)
|
||||
- 后续计划:若【需确认】项实测发现 `DELETE` 对过去日期同样拦截,需另开工单放开残留行的过去日期取消
|
||||
|
||||
> **2026-09-26 订正(#8371)**:该「另开工单」已完成,即 #8371——已实测坐实过去拦截、已修复并验证放行。见 `26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md`。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
@@ -12,7 +12,7 @@ frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "backend_status=deployed:测试服 hl-order-service-v3 双实例(8086 / 8186)当前运行 sha=843de6401。本单修复提交 bfc10966e 不是该 sha 本身而是它的祖先,故用两条互相独立的判据:①git merge-base --is-ancestor bfc10966e 843de6401 → 退出码 0(是祖先),且 deploy-status.sh 登记 dev-v3 / 843de6401 / BEHIND=0/N;②jar mtime 2026-09-22 18:55:47,晚于 bfc10966e 的提交时刻 2026-09-22 15:24:50 +0800。两条之外还有第三条活体判据:本单契约在 18:5x 经网关实测四组请求取证(见第八节),其中三组的返回值在改动前不可能出现。gateway_status=not_required:本单零网关改动。PUT /v3/admin/group-batch/{productBatchId}/staff 是存量端点、路径未变,命中既有通配路由 Path=/v3/admin/**,未引入任何新路径段,也未改 hl-gateway 的路由或 Nacos 配置。frontend_status=not_required:本单收紧的是一个前端目前不会构造的取值。判据——对 mmg/hl-ui origin/v2.1 的调用方 grep,scopeRoles 一律由 roleMeta.memberRoles 这类字面量常量数组提供(GroupBatchStaffConfigModal.vue:153-154 写死 ['GUIDE','LEADER'] 与 ['PHOTOGRAPHER']),无任何一处会产出 null / 空串 / 纯空白元素;对 [\"\"] 与 [\" \"] 本单只改 message 文案长度、不改 code。⚠️ 覆盖边界:这是「现有实现不会命中」,不是「将来也不会」——若前端改为由下拉或接口回填拼 scopeRoles,未选中的空值会直接命中本单的 400,请按第四节的契约处理。"
|
||||
status_note: "backend_status=deployed:测试服 hl-order-service-v3 双实例(8086 / 8186)当前运行 sha=843de6401。本单修复提交 bfc10966e 不是该 sha 本身而是它的祖先,故用两条互相独立的判据:①git merge-base --is-ancestor bfc10966e 843de6401 → 退出码 0(是祖先),且 deploy-status.sh 登记 dev-v3 / 843de6401 / BEHIND=0/N;②jar mtime 2026-09-22 18:55:47,晚于 bfc10966e 的提交时刻 2026-09-22 15:24:50 +0800。两条之外还有第三条活体判据:本单契约在 18:5x 经网关实测四组请求取证(见第八节),其中三组的返回值在改动前不可能出现。gateway_status=not_required:本单零网关改动。PUT /v3/admin/group-batch/{productBatchId}/staff 是存量端点、路径未变,命中既有通配路由 Path=/v3/admin/**,未引入任何新路径段,也未改 hl-gateway 的路由或 Nacos 配置。frontend_status=not_required:本单收紧的是一个前端目前不会构造的取值。判据——对 mmg/hl-ui origin/v2.1 的调用方 grep,scopeRoles 一律由 roleMeta.memberRoles 这类字面量常量数组提供(GroupBatchStaffConfigModal.vue:153-154 写死 ['GUIDE','LEADER'] 与 ['PHOTOGRAPHER']),无任何一处会产出 null / 空串 / 纯空白元素;对 [\"\"] 与 [\" \"] 本单只改 message 文案长度、不改 code。⚠️ 覆盖边界:这是「现有实现不会命中」,不是「将来也不会」——若前端改为由下拉或接口回填拼 scopeRoles,未选中的空值会直接命中本单的 400,请按第四节的契约处理。 | 2026-09-22 mmg 复核:grep 实证前端唯一构造点 ROLE_META.memberRoles 为字面量常量数组,零行为改动;orderV2GroupBatch.js JSDoc 补记新口径(拼接串禁全等、回填须 filter(Boolean)),见 hl-ui@6bf97829e"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
@@ -7,12 +7,12 @@ author: "wx(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: ""
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "bc4024952daa784232abe6fbf56b658b57361bb3"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-22"
|
||||
status_note: "2026-09-22 mmg 交付:ShareGroupEditModal 新建/编辑选举(浏览态三态 selectable 不误禁、shareGroupId 复算恢复本关系成员并预勾、members 全集提交、605036 确认重发、602106 刷新重选),ShareGroupPanel 加入口,抽屉透传 serviceDates;checkpoint 全绿 26 测试"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
@@ -202,7 +202,8 @@ GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candida
|
||||
|
||||
🔴 **但浏览态下 `selectable` 并不总是 `null`**:有两条判据不依赖 `resourceId`,浏览态照样能判死——
|
||||
- **已属别的 ACTIVE 共用关系** → `selectable=false`,`unselectableReason=ALREADY_IN_ANOTHER_GROUP`
|
||||
- **团期身份缺失**(`group_batch_id` / `requirement_id` 为空或团期不符)→ `selectable=false`,`unselectableReason=CROSS_BATCH`
|
||||
- **团期身份缺失**(`requirement_id` 为空,或团期与本团不符)→ `selectable=false`,`unselectableReason=CROSS_BATCH`
|
||||
⚠️ **订正 2026-09-22**:本行原写「`group_batch_id` / `requirement_id` 为空」,**`group_batch_id` 那一半不成立**——取数条件是 `eq(group_batch_id, ?)`,`group_batch_id` 为空的派单行**结构上查不出来**、压根不在清单里(见第六节「4. 覆盖范围」)。所以前端**不必**为「`group_batch_id` 为空 + `CROSS_BATCH`」写分支,那个组合不可能出现;会以 `CROSS_BATCH` 出现在清单里的,只有 `requirement_id` 为空或团期不符这两种。
|
||||
|
||||
⇒ 前端在浏览态**不能**把 `selectable == null` 当成 `false` 禁用勾选,也**不能**当成 `true` 放开;但**要照常渲染 `selectable === false` 的置灰与原因**——那些行在浏览态就已经定死了,选了具体资源也不会变回可选。
|
||||
|
||||
@@ -295,7 +296,7 @@ GET /admin/fleet/group-dispatch/batches/2102125467954044929/share-member-candida
|
||||
|
||||
| 值 | 触发条件 | 浏览态(未传 `resourceId`)能否判出 | 对应写口错误码 |
|
||||
|---|---|---|---|
|
||||
| `CROSS_BATCH` | `group_batch_id` / `requirement_id` **为空**,或团期与本团不符 | ✅ 能 | 602103 |
|
||||
| `CROSS_BATCH` | `requirement_id` **为空**,或团期与本团不符。⚠️ 订正 2026-09-22:**不含 `group_batch_id` 为空**,那类行结构上查不出来、不在清单里(见第六节「4. 覆盖范围」) | ✅ 能 | 602103 |
|
||||
| `NOT_OCCUPYING` | 派单不在**可改派的在飞态**,或当日**标记不用车** | ❌ 不能(需 `resourceId`) | 602102 |
|
||||
| `ALREADY_IN_ANOTHER_GROUP` | 本行已属某个 ACTIVE 共用关系(此时 `shareGroupId` **非空**) | ✅ 能 | 602106 |
|
||||
| `GROUP_DISPATCH_INVALID` | 团级配车行**当日不在此资源上** | ❌ 不能(需 `resourceId`) | 602105 |
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8193"
|
||||
title: "通知中心: 团期房务三个事件补齐配置行,管理后台站内信新增三类消息;两条已无发布方的询房配置行删除"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修复"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "本条不改变 AdminMessageRespVO / AdminMessagePageReqVO 的字段结构,也不新增任何端点;只在 notification_event_config 里补三行、删两行、关两处通道。frontend_status=not_required 的依据是实证而不是估计:三类新消息的 link 落在 /housekeeper/grab-pool-group,hl-ui(origin/v2.1) 的 src/views/notification/MyMessages/jumpBiz.js 对 link 的处理是通用的(非 /pages/ 前缀即直接 router.push),src/router 已注册该路由且 src/views/housekeeper/grab-pool-group/index.vue 真实存在,V20260922_210 已把该路由纳入白名单 ⇒ 这三类消息在当前已交付的前端代码上直接可点可跳,mmg 零改动。gateway_status=not_required:零新增路由,受影响的 GET /admin/message/list 是既有端点。backend_status=deployed:hl-user-service 已滚动到测试服,Flyway 20260922.211 success=1(installed_on 2026-09-22 21:59:35),并在自建团期夹具(groupBatchId=2102401449357197314)上走完「整体确认需求 / 房务认领 / 房务释放 → 站内信产出 → GET /admin/message/list 读回」全链路,三个事件各 18 条 ADMIN_INAPP 且 status 全为成功,实测读数见正文第五节。⚠️ admin_message 是快照表:link 在 createMessage 落库那一刻按当时模板一次性渲染写进行内,不是按需渲染 ⇒ 本条只影响生效后新产生的消息,历史行不回刷。 mmg 2026-09-23 复核: grep 实证 jumpBiz.js resolveJumpLink 对非 /pages/ 前缀 link 直喂 router.push(通用分支无事件码分支)、src/router 已注册 housekeeper/grab-pool-group、src/views/housekeeper/grab-pool-group/index.vue 真实存在——三类新消息在当前已交付代码上零改动可点可跳,not_required 成立。"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 通知中心: 团期房务三个事件补齐配置行,管理后台站内信新增三类消息;两条已无发布方的询房配置行删除
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-user-service(通知分发 / 站内信收件箱 / 配置表;Flyway `V20260922_211`)+ hl-order-service-v3(通知发布方,本次仅注释)
|
||||
> **PR**: [#8203](https://git.1814.love:8443/wx/HL/pulls/8203)
|
||||
> **Issue**: [#8193](https://git.1814.love:8443/wx/HL/issues/8193)
|
||||
> **日期**: 2026-09-22
|
||||
> **影响范围**: 管理后台站内信中心(`GET /admin/message/list`)里 HOUSE 类通知的**条目数量与种类**,不涉及字段结构
|
||||
|
||||
---
|
||||
|
||||
## 一、一句话
|
||||
|
||||
团期房务的三个事件此前**一直在发**,但 `notification_event_config` 里没有对应配置行,分发器因此不产出任何消息。本次补齐配置行,房管角色(`ROOM_MANAGER`)的站内信收件箱从此会新增三类消息。
|
||||
|
||||
## 二、新增的三类消息(前端看得见)
|
||||
|
||||
| `event_code` | 触发时机 | 标题形态 |
|
||||
|---|---|---|
|
||||
| `GROUP_BATCH_REQUIREMENT_CONFIRMED` | 团期需求整体确认放行 | 整团房需求已确认:{团期名} |
|
||||
| `GROUP_BATCH_HOUSE_CLAIMED` | 团期房务被认领(含接管) | 团期房务已认领:{团期名} |
|
||||
| `GROUP_BATCH_HOUSE_RELEASED` | 团期房务被释放回池 | 团期房务已释放:{团期名} |
|
||||
|
||||
三类消息在 `GET /admin/message/list` 里的字段取值(**实测报文**,非推断):
|
||||
|
||||
```json
|
||||
{
|
||||
"messageId": "2102401964484829186",
|
||||
"categoryCode": "SYSTEM",
|
||||
"title": "整团房需求已确认:Q202612082102401392893431809",
|
||||
"content": "团期 Q202612082102401392893431809 的房需求已整体确认,放行 1 户、跳过 0 户,可开始配房。",
|
||||
"link": "/housekeeper/grab-pool-group?groupBatchId=2102401449357197314",
|
||||
"bizId": "2102401449357197314",
|
||||
"bizType": "HOUSE",
|
||||
"isRead": 0,
|
||||
"messageType": "NORMAL",
|
||||
"messageTypeLabel": "普通消息",
|
||||
"kind": "NOTIFY",
|
||||
"senderName": null,
|
||||
"conversationKey": null,
|
||||
"teamMessage": false
|
||||
}
|
||||
```
|
||||
|
||||
**收件人**:`wework_receiver_type=HOUSE_TEAM` ⇒ 分发器取 `ROOM_MANAGER` 角色下 `status='ACTIVE'` 的全部管理员,测试库当前 18 个账号,每个事件各落 18 行 `admin_message`。
|
||||
|
||||
## 三、跳转行为:前端零改动即可用
|
||||
|
||||
`link` 模板是 `/housekeeper/grab-pool-group?groupBatchId=${groupBatchId}`,三类消息共用。
|
||||
|
||||
- `jumpBiz.js` 的 `resolveJumpLink` 对**非 `/pages/` 前缀**的 link 直接返回并 `router.push`,不做事件码分支 ⇒ 这三类消息走的是已交付的通用分支。
|
||||
- `canJumpToBiz` 对 HOUSE 非聊天行的判据是「有没有可用 link」,本条三类消息 link 非空 ⇒ **跳转入口会正常渲染**。
|
||||
- `/housekeeper/grab-pool-group` 已在 `src/router` 注册,`src/views/housekeeper/grab-pool-group/index.vue` 真实存在;后端 `V20260922_210` 也已把该路由纳入 link 白名单。
|
||||
|
||||
**⇒ mmg 不需要为本条写任何代码。**
|
||||
|
||||
## 四、契约的覆盖边界(写在脸上,供前端判断要不要做增强)
|
||||
|
||||
1. **目标页当前不消费 `?groupBatchId=` 这个查询参数**。`grab-pool-group/index.vue` 里没有任何 `route.query` / `useRoute` 读取(阳性对照:同目录下另有 5 个视图文件确有该用法,所以这不是我查不到)。同模块的 `todos/index.vue` 对 `?inquiryId=` 同样不读——**这是房务模块既有且已随 #8182 交付的约定,不是本条引入的新问题**。因此点进去落到的是抢单池列表页本身,不是定位到该团期。要做「直达该团期」的增强,前端读这个 query 即可,后端已经把 id 送到了。
|
||||
2. **`bizId` 不是路由键**。三类消息的 `bizId` 等于 `groupBatchId`、`bizType=HOUSE`,但 HOUSE 域的 `bizId` 在不同事件下有 `hotelId` / `groupBatchId` / `orderId` 三种含义,禁止拿它反推页面——唯一正确的跳转来源是 `link`。这条约束已固化在 `HouseNotificationPublisher` 类注释里。
|
||||
3. **`admin_message` 是快照表**。`link` 在消息落库那一刻按当时模板渲染写进行内,之后改模板不影响已存在的行。本条生效时刻为 2026-09-22 21:59:35,此前产生的消息不受影响,也不做批量回刷。
|
||||
4. **消息只发给房管角色**。非 `ROOM_MANAGER` 的管理员收件箱里不会出现这三类消息,这是预期行为不是漏发。
|
||||
|
||||
## 五、前端**看不见**的三处配置变更(列出以免被误读为回归)
|
||||
|
||||
| 变更 | 对前端的影响 |
|
||||
|---|---|
|
||||
| 删除 `HOUSE_INQUIRY_TIMEOUT`、`HOUSE_INQUIRY_ESCALATED` 两行配置 | **无**。这两个事件的发布方随 #4470「天级确认流程」改造已被一并删除,配置行留着也从不产出消息;删前已按「字面量 / 前缀拼接 / 非 Java 载体」三种形态穷举 + 阳性对照查证零发布方。 |
|
||||
| `HOUSE_HOTEL_SWAPPED_NEW_HOTEL` / `_OLD_HOTEL` 的 `inapp_enabled` 由 1 改 0(**行保留**) | **无**。这两个收件人类型是酒店联系人,不在分发器的站内信收件人白名单里,站内信对他们结构上不可达;`inapp_enabled=1` 是假象,自出生起产出为零。保留行是为了将来走短信时只改一位。 |
|
||||
| `TRAVELER_INCOMPLETE_DAILY` 的企微三列关闭(**行保留**) | **无**。该事件的 `wework_receiver_type` 原值 `'USER'` 不是合法收件人类型,企微 14 条全部发送失败;同一事件的站内信 / 短信 / 小程序三条通道**保持原样开启,一条不少**。 |
|
||||
|
||||
## 六、实测读数
|
||||
|
||||
**配置表活体**(2026-09-22,测试库):
|
||||
|
||||
```
|
||||
GROUP_BATCH_HOUSE_CLAIMED HOUSE inapp=1 /housekeeper/grab-pool-group?groupBatchId=${groupBatchId} HOUSE_TEAM
|
||||
GROUP_BATCH_HOUSE_RELEASED HOUSE inapp=1 /housekeeper/grab-pool-group?groupBatchId=${groupBatchId} HOUSE_TEAM
|
||||
GROUP_BATCH_REQUIREMENT_CONFIRMED HOUSE inapp=1 /housekeeper/grab-pool-group?groupBatchId=${groupBatchId} HOUSE_TEAM
|
||||
```
|
||||
|
||||
**发送流水**(`notification_send_log`,按自建夹具 `biz_id=2102401449357197314` 聚合):
|
||||
|
||||
```
|
||||
GROUP_BATCH_REQUIREMENT_CONFIRMED ADMIN_INAPP status=0(SUCCESS) 18 行 22:17:44
|
||||
GROUP_BATCH_HOUSE_CLAIMED ADMIN_INAPP status=0(SUCCESS) 18 行 22:18:08
|
||||
GROUP_BATCH_HOUSE_RELEASED ADMIN_INAPP status=0(SUCCESS) 18 行 22:18:18
|
||||
```
|
||||
|
||||
只有 `ADMIN_INAPP` 一个通道、无 WEWORK/SMS 行,与配置 `inapp_enabled=1 / wework_enabled=0` 一致。18 这个数与「`ROOM_MANAGER` 角色下 `status='ACTIVE'` 的账号共 18 个」是两个独立口径,互相吻合。
|
||||
|
||||
**link 渲染逐字核对**:报文里三条 link 完全相同,其中 `groupBatchId=2102401449357197314` 与自建团期 id 逐字相等(19 位),模板里无残留 `${` 占位。
|
||||
|
||||
**全表断言**:`notification_event_config` 共 63 行 = 合法收件人类型 34 行 + `IS NULL` 29 行 + **非法 0 行**;阳性对照(把 `NOT IN` 改 `IN` 同一批 14 个常量)返回 34 行,证明该断言 SQL 本身能命中。
|
||||
@@ -0,0 +1,455 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8195"
|
||||
title: "团期「查看需求」页五处缺口: 户级团号 teamNo / 未提交户进列表 / 接送机批量确认新端点 / 车型字典校验 / 团期 endDate"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "7df2926240c51d48c31fa3c13962dd0f42b9c9ca"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "backend_status=deployed: hl-order-service-v3 已滚动到测试服,deploy-status.sh 读数 dev-v3 / 62449e550 / DEPLOYED_AT 2026-09-22 22:22:58 / STATE=ok,62449e550 即本单合并提交本身;九条验收项全部在测试服网关上取到活体读数,原始报文逐条落盘。gateway_status=not_required: 新端点路径落在 hl-gateway 既有 /v3/admin/** 通配上,零新增路由——判据不是推断而是实测:该路径返 400「请至少选择一个要确认的子订单」(业务校验),而故意写错的同前缀路径返 404「接口不存在」,两种报文形态不同 ⇒ 路由确实存在。frontend_status=pending: 本条新增 4 个响应字段、1 个端点,且改了 householdCount 的口径,hl-ui 需要改;hl-ui(origin/v2.1) 里 src/api/orderV2GroupBatch.js 两处 JSDoc 描述的是改前契约,改后已不准确,逐行列在第六.6 节。⚠️ 契约边界:本接口上「从未提交需求的户」与「提交后被打回的户」完全同形(两者都是 requirements:[] + status:null),不可区分,依据 GroupVehicleHouseholdsRespVO.java:169-171。 mmg 2026-09-23 交付: hl-ui@7df29262 feat(order-v2) 五处缺口全落地——户卡 teamNo/统计行 endDate/未提交户空卡(中性文案)/批量确认(failedCount 判成败+reason 明细)/车型字典下拉(存量非字典值 rule 前置拦);checkpoint 全量绿,相关 106 例测试全过。"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期「查看需求」页五处缺口: 户级团号 teamNo / 未提交户进列表 / 接送机批量确认新端点 / 车型字典校验 / 团期 endDate
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
三条,改前改后行为不同,按这个顺序看:
|
||||
|
||||
1. **`orderNo` 从来就不是团号。** 改前两个 households 接口的 Swagger 把 `orderNo` 标成「子订单团号」、example 写 `GT-26-0081`,照着当团号渲染出来的其实是订单号 `HL20260922210334521`。本次**新增 `teamNo` 字段**承载真团号,`orderNo` 字段**保留不删**(前端 `v-for :key` 在用),但注解已订正为「子订单编号(非团号)」。**团号请改读 `teamNo`。**
|
||||
2. **`vehicleRowCount` 不再恒 ≥ `householdCount`。** 改前「没提交过用车需求的户」根本不出现在车侧响应里;改后它们**进列表**(`requirements: []`、`status: null`),`householdCount` 随之变成「应报车的户数 = `households` 长度」。原先「行数 ≥ 户数」这个不变量**作废**,别再拿它写断言。实测基线读数 `householdCount=5 / countedHouseholdCount=0 / vehicleRowCount=2`。
|
||||
3. **接送机批量确认允许部分成功,且部分失败时 HTTP 仍是 `code:200` / `success:true`。** 失败的户在 `data.failed[]` 里逐条给 `orderId + errorCode + reason`。**不要用 `success` 判断「是不是全成了」**,要看 `failedCount`。
|
||||
|
||||
## 一、背景
|
||||
|
||||
工单 #8195,wx 在团期「查看需求」页上点出的五处缺口,合并为一个 PR(#8204,squash `62449e550`)。五处分别对应:缺陷 1 团号、缺陷 2 未提交户不可见、缺陷 3 接送机无批量确认、缺陷 4 车型无字典校验、缺陷 5 缺团期结束日。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 批量确认接送机需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` | 新增 | 允许部分成功;幂等窗口 120s |
|
||||
| 2 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 修改 | +`endDate` +`teamNo` +户级`status`/`statusName`;未提交户进列表 |
|
||||
| 3 | 团期子订单订房记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` | 修改 | +`endDate` +`teamNo`;`orderNo` 注解订正 |
|
||||
| 4 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 修改 | 车型必须命中字典,否则 809119 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 批量确认接送机需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm`
|
||||
|
||||
**VO**: `TransferBatchConfirmReqVO` → `Result<TransferBatchConfirmRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「查看需求」页「用车」板块,团期管理员勾选若干户的接送机需求,一次性确认并转交车务。等价于逐户调 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER`,区别是**允许部分成功**:整批里只要有一户能确认,接口就按业务成功返回,失败户单独列出而不回滚成功户。权限码与整团确认、按户打回同为 `group-batch:demand:confirm`——同一个 Tab 里同一批人的同一类动作,分码会出现「能逐户放行却不能批量放行」这种前端无法向运营解释的组合。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||
| `orderIds` | body | Long[] | 是 | `@NotEmpty`、`@Size(max=200)` | 待确认的子订单 ID,1~200 户,**服务端去重** |
|
||||
| `dispatchRemark` | body | String | 否 | `@Size(max=500)` | 确认备注,**整批共用**,提供给车队 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `groupBatchId` | String | 团期 ID(`ToStringSerializer`) |
|
||||
| `requestedCount` | int | 去重后的待确认户数,**恒等于 `successCount + failedCount`**,可用于对账 |
|
||||
| `successCount` | int | 确认成功的户数 |
|
||||
| `failedCount` | int | 确认失败的户数;**> 0 时请展示 `failed` 明细,不要只提示「部分成功」** |
|
||||
| `succeededOrderIds` | String[] | 成功的子订单 ID,按请求顺序,字符串形态防 JS 精度丢失 |
|
||||
| `failed` | FailedItem[] | 失败明细,按请求顺序;**全部成功时是空数组,不是 null** |
|
||||
| `failed[].orderId` | String | 子订单 ID |
|
||||
| `failed[].errorCode` | Integer | 业务错误码;**非业务异常(系统故障)时为 null** |
|
||||
| `failed[].reason` | String | 已渲染的中文报文,可直接展示 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2102383303678189570/requirement/transfer/batch-confirm
|
||||
Content-Type: application/json
|
||||
|
||||
{"orderIds":[2102383417822003201,2102383458422841346],"dispatchRemark":"11/20 首都机场接"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
全部成功(测试服实测原文,`ac/06-batch-confirm-1.json`):
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"groupBatchId":"2102383303678189570","requestedCount":2,"successCount":2,"failedCount":0,"succeededOrderIds":["2102383417822003201","2102383458422841346"],"failed":[]},"traceId":null,"success":true}
|
||||
```
|
||||
|
||||
部分成功(测试服实测原文,`ac/07-partial-fail.json`)。**注意 `code` 是 200、`success` 是 true,但有一户没确认成功**:
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"groupBatchId":"2102383303678189570","requestedCount":2,"successCount":1,"failedCount":1,"succeededOrderIds":["2102405916668440577"],"failed":[{"orderId":"2102383417822003201","errorCode":582083,"reason":"需求状态不允许此操作,请检查当前状态"}]},"traceId":null,"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`orderIds` 传空数组或不传时不进业务逻辑,直接被参数校验拦下,不产生任何写操作。`succeededOrderIds` 与 `failed` 在任何成功响应里都是数组,不会是 `null`——全成功时 `failed` 是 `[]`,全失败时 `succeededOrderIds` 是 `[]`,前端可以无条件 `.map()` 而不必先判空。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
整批被拒的两种形态(幂等拦截为测试服实测原文 `ac/08-batch-confirm-retry.json`):
|
||||
|
||||
```json
|
||||
{"code":400,"message":"请至少选择一个要确认的子订单"}
|
||||
{"code":100502,"message":"接送机需求确认处理中,请勿重复提交","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
单户被拒**不走错误响应**,而是进上面 `data.failed[]`,典型 `errorCode` 为 `582083`「需求状态不允许此操作,请检查当前状态」(例如该户已经是 `PENDING`)。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **幂等窗口 120 秒**:`@Idempotent` 的 key 由 `groupBatchId` + `orderIds` 共同决定 ⇒ **换一批 `orderIds` 不受上一次影响**,同一批在 120s 内第二次必被 `100502` 拒。
|
||||
- **部分成功是设计,不是异常**:失败户不会被静默跳过,也不会把整批回滚。
|
||||
- **确认只改需求状态,不产生派车记录**:本阶段只把 `order_vehicle_requirement.status` 与 `order_main.vehicle_control_status` 从 `PENDING_REVIEW` 置为 `PENDING`;实测两次调用前后 `fleet_assignment` 按 `order_id` 过滤 `COUNT(*)` 均为 0。真正的派车分单是车队侧后续独立动作。
|
||||
- **重复的 `orderIds` 服务端去重**:`requestedCount` 是去重**后**的数,前端拿它对账不会因为自己传重而对不上。
|
||||
- **响应体里不含刷新后的行**:确认成功后需要重新拉一次 `vehicle-households` 才能看到新状态。
|
||||
|
||||
### 2. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
|
||||
|
||||
**VO**: `Result<GroupVehicleHouseholdsRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「查看需求」页「用车」板块下半块的逐户列表。本次把它从「已提交需求的户的列表」改成「应报车的户的列表」——运营需要看见「谁还没交」,而改前那些户根本不出现在响应里,页面上无从催办。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||
| `kind` | query | String | 否 | `TRAVEL` / `TRANSFER` | **不传 = 两类都返**(与提交侧「不传按 TRAVEL」的缺省相反,此处未改) |
|
||||
|
||||
#### 出参
|
||||
|
||||
新增 4 个字段、2 个既有字段口径变化,其余未动:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `endDate` | String | **新增**。团期结束日期 `yyyy-MM-dd`;团期未定结束日时为 null(与团期详情 `endDate` 同源) |
|
||||
| `households[].teamNo` | String | **新增**。子订单团号,取 `order_main.team_no`;**未付订金尚未分配时原样返 null**,后端不兜底成 `orderNo`、不回退空串 |
|
||||
| `households[].status` | String | **新增**(户级)。**null = 该户一份用车需求都没提交**;非 null 时取展示序首条(TRAVEL 优先)的状态 |
|
||||
| `households[].statusName` | String | **新增**(户级)。与 `status` 同一条需求行的中文名;`status` 为 null 时本字段也为 null |
|
||||
| `householdCount` | int | **口径变更**。改前 = 有活跃需求行的户数;改后 = 应报车的户数,恒等于 `households` 长度,**含一份都没提交的户**。仍按 `orderId` 去重(一户同时报行程用车与接送机只算 1 户) |
|
||||
| `vehicleRowCount` | int | 口径未变、**关系变了**。= Σ 各户 `requirements` 长度,未提交户贡献 0 行 ⇒ **可能小于 `householdCount`** |
|
||||
| `countedHouseholdCount` | int | 口径未变、**分母变了**。= 有活跃 TRAVEL 行的户数。`householdCount − countedHouseholdCount` 从改前的「只报了接送机的户」变成「只报了接送机的户 **+ 一份都没提交的户**」 |
|
||||
| `households[].orderNo` | String | 值与形态都没变(`"HL" + yyyyMMddHHmmssSSS`,定长 19),只是 Swagger 不再谎称它是团号 |
|
||||
| `requirements[].status` | String | **未变**,早就有。逐条的权威状态在这里,不在户级 `status` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102383303678189570/requirement/vehicle-households
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服实测(`ac/00-vehicle-households.json`)顶层为 `householdCount=5`、`countedHouseholdCount=0`、`vehicleRowCount=2`、`endDate="2026-11-21"`,五户的关键字段:
|
||||
|
||||
```json
|
||||
[{"orderNo":"HL20260922210334521","teamNo":"26-3724","status":null,"requirements":[]},
|
||||
{"orderNo":"HL20260922210348601","teamNo":null,"status":null,"requirements":[]},
|
||||
{"orderNo":"HL20260922210358692","teamNo":null,"status":null,"requirements":[]},
|
||||
{"orderNo":"HL20260922210401823","teamNo":"26-0847","status":"PENDING_REVIEW","requirements":["…1 条"]},
|
||||
{"orderNo":"HL20260922210411494","teamNo":"26-7970","status":"PENDING_REVIEW","requirements":["…1 条"]}]
|
||||
```
|
||||
|
||||
三个计数两两不等,正好演示新口径:5 户全在列表里,只有 2 户提交过,且两条都是 TRANSFER,所以计入车侧汇总的 TRAVEL 户数是 0。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
该户没提交任何用车需求时 `requirements` 是 `[]`(**空数组,不是 null**)、`status` 与 `statusName` 均为 null;团期未定结束日时 `endDate` 为 null;未付订金时 `teamNo` 为 null。整团一户都没有时三个计数为 0、`households` 为 `[]`,接口仍返 200。以上四种降级都不会让接口报错,前端需要各自有占位显示。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次未改,沿用团期段位错误码:
|
||||
|
||||
```json
|
||||
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
另有 `589507`(`GROUP_BATCH_PERMISSION_DENIED`,团期操作/读取被拒)由拦截器透 `message`,前端直接展示即可。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **户级 `status` 是折叠态用的,不是权威状态**:一户可能同时有 TRAVEL 与 TRANSFER 两条活跃行、状态各自独立(例如 TRAVEL 已放行 `PENDING`、TRANSFER 还在 `PENDING_REVIEW`),户级 `status` 只取展示序第一条。**按条判断一律读 `requirements[].status`。**
|
||||
- 🔴 **「从未提交」与「提交后被打回」在本接口上不可区分**:打回 = 原地置 `REJECTED_*` + `is_active=0`,失活行不进 `requirements` ⇒ 被打回的户同样是 `requirements: []` + `status: null`,与从未提交的户**完全同形**。依据 `GroupVehicleHouseholdsRespVO.java:169-171`。要把这两种人分开,本接口给不出判据。
|
||||
- **`teamNo` 与 `orders` 接口同源同值**:取 `order_main.team_no`,不是 `order_group_batch.batch_no`(那是整团一个值,放在逐户列表上每行都一样,这一列就没有分辨力了)。三个读口(hotel-households / vehicle-households / orders)的 `teamNo` 实测逐字符相等。
|
||||
|
||||
### 3. 团期子订单订房记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households`
|
||||
|
||||
**VO**: `Result<GroupHotelHouseholdsRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「查看需求」页「用房」板块下半块的逐户列表,与 `requirement-summary` 并列调用。本次只补两个字段并订正一处注解,列表口径没动——用房侧本来就包含未提交的户(`status=null`、`days=[]`),这次是车侧向它对齐,不是它变了。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `endDate` | String | **新增**。团期结束日期 `yyyy-MM-dd`,未定时 null,与车侧同源同值 |
|
||||
| `households[].teamNo` | String | **新增**。同车侧,取 `order_main.team_no`,未付订金时 null |
|
||||
| `households[].orderNo` | String | 注解订正(「子订单团号」→「子订单编号(非团号)」),**值未变** |
|
||||
| `households[].status` | String | **未变**。用房侧本来就有(未提交时为 null) |
|
||||
| `households[].statusName` | String | **未变**。用房侧本来就有 |
|
||||
| `householdCount` | int | **未变**。与 `countedHouseholdCount` 的差值仍是「未计入汇总(打回 / 未提交)的户数」 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102383303678189570/requirement/hotel-households
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服实测(`ac/00-hotel-households.json`),`endDate` 为 `"2026-11-21"`,五户 `teamNo` 与车侧、与 `GET .../orders` 三方逐字符相等:
|
||||
|
||||
```json
|
||||
[{"orderNo":"HL20260922210334521","teamNo":"26-3724"},
|
||||
{"orderNo":"HL20260922210348601","teamNo":null},
|
||||
{"orderNo":"HL20260922210358692","teamNo":null},
|
||||
{"orderNo":"HL20260922210401823","teamNo":"26-0847"},
|
||||
{"orderNo":"HL20260922210411494","teamNo":"26-7970"}]
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
未付订金的户 `teamNo` 为 null;团期未定结束日时 `endDate` 为 null。既有的降级行为一律未动:需订房但未提交的户仍会列出(`status=null`、`days=[]`),客户自订晚仍会列出且 `hotels=[]`,打回户仍列出且 `countedInSummary=false`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次未改,与车侧同段位:
|
||||
|
||||
```json
|
||||
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **用房侧的列表口径没有跟着车侧一起改**:它本来就含未提交户,本次只补 `teamNo` 与 `endDate` 两个字段。
|
||||
- **「汇总 == Σ 子订单」这条既有硬约束不受影响**:汇总逐日间数仍等于本接口 `countedInSummary=true` 各户逐日加总,差值仍是 `householdCount − countedHouseholdCount`。
|
||||
- **`orderNo` 的排序契约未变**:`households` 仍按 `orderNo` 升序,新增 `teamNo` 不参与排序——**不要改用 `teamNo` 排序**,它可以为 null。
|
||||
|
||||
### 4. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `GroupVehicleRequirementSaveReqVO` → `Result<GroupVehicleRequirementRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期管理员新增 / 编辑整团的正式行程用车需求,一次全量替换整份(主表 + 全部分组 + 全部逐日行)。本次给分组里的车型加了字典校验:改前前端传什么就落什么,运营填错的车型要等到车队派车时才暴露;改后在保存这一步就拒。
|
||||
|
||||
#### 入参
|
||||
|
||||
结构不变,仅新增一条约束:
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传 |
|
||||
| `groups[].vehicleType` | body | String | 是 | **新增:必须命中车型字典** | 车型大类编码,须存在且未下线,从下拉项取 |
|
||||
| `groups[].groupName` | body | String | 是 | — | 组名,校验失败时会被写进错误报文,便于定位是哪一组 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `groups[].vehicleType` | String | 校验通过后回显的车型编码,未变 |
|
||||
| `groups[].vehicleTypeName` | String | 车型中文名,后端按字典下发,**禁前端自映射** |
|
||||
|
||||
其余字段结构不变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{"groups":[{"groupName":"AC9-BAD","vehicleType":"minivan"}]}
|
||||
```
|
||||
|
||||
`vehicleType` 须取自 `GET /admin/fleet/vehicle-types/list` 的下拉项;上例的 `minivan` 不在字典内,用于演示校验被触发。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
合法车型实测(`ac/09-good-type-4.json` 节选):
|
||||
|
||||
```json
|
||||
{"code":200,"message":"成功","data":{"groups":[{"vehicleType":"bus","vehicleTypeName":"大巴系列"}]},"success":true}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
车型字典为空时**不放行**(fail-closed),不会退化成「不校验」——宁可让保存失败并提示运营去维护字典,也不能把一批查不到名字的车型放进正式需求,那会在车队侧变成一堆无法派车的行。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非法车型实测原文(`ac/09-bad-type.json`),报文里带**组名**,前端可直接定位到是哪一组填错:
|
||||
|
||||
```json
|
||||
{"code":809119,"message":"第 AC9-BAD 组的车型 minivan 不在车型字典内(不存在或已下线),请从下拉项中选择","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **车型必须从 `GET /admin/fleet/vehicle-types/list` 的返回里选**,不要在前端硬编码枚举——字典行可被下线,下线后同一个编码就会被拒。
|
||||
- **本端点另有两条既有前置会先于车型校验触发**(本次未改):团期阶段守卫(`RECRUITING` 阶段被 `589501`「团期状态不允许当前操作」拒,需先成团)、逐日乘车分组必须覆盖全团在团户与全部行程日(否则 `809109`「子订单 {0} 的 {1} 没有被任何乘车分组覆盖」)。
|
||||
- **整份全量替换**:保存即覆盖,前端提交前必须带上未改动的分组与逐日行,否则会被删掉。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误对照
|
||||
|
||||
| 场景 | ❌ 错误 | ✅ 正确 |
|
||||
|---|---|---|
|
||||
| 渲染团号列 | 读 `orderNo` | 读 `teamNo`;为 null 时显示占位符,**不要回落成 `orderNo`** |
|
||||
| 表头「共 N 户」 | 用行数或自行推算 | 取 `householdCount`(与 `households.length` 恒等) |
|
||||
| 判断批量确认结果 | `if (res.success) { 提示全部成功 }` | `if (res.data.failedCount > 0) { 展示 res.data.failed 明细 }` |
|
||||
| 判断某条需求的状态 | 读户级 `status` | 读 `requirements[i].status` |
|
||||
| 车型下拉 | 前端写死枚举数组 | 调 `GET /admin/fleet/vehicle-types/list` |
|
||||
| 列表排序 | 改用 `teamNo` 排序 | 仍按 `orderNo`(`teamNo` 可为 null) |
|
||||
| 传 ID | `Number(orderId)` | 雪花 ID 一律字符串透传 |
|
||||
|
||||
### 状态切换后的必要动作
|
||||
|
||||
批量确认成功后,被确认户的 `order_vehicle_requirement.status` 与 `order_main.vehicle_control_status` 都变为 `PENDING`。响应体里不含刷新后的行,需要重新拉一次 `vehicle-households`。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
批量确认端点写两张表:`order_vehicle_requirement.status`、`order_main.vehicle_control_status`,均 `PENDING_REVIEW → PENDING`,调用前后逐户 `SELECT` 核对过。**不写 `fleet_assignment`**(前后均 `COUNT(*)=0`)。清单里的 2、3 两个读接口不写库。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 情形 | 行为 |
|
||||
|---|---|
|
||||
| 未付订金的户 | `teamNo` 为 `null`(`team_no` 此时尚未分配),前端需要占位显示 |
|
||||
| 一份用车需求都没提交的户 | 进车侧列表,`requirements: []`、`status: null`、`statusName: null` |
|
||||
| 提交后被打回的户 | **与上一行完全同形,本接口不可区分** |
|
||||
| 一户同时有 TRAVEL + TRANSFER | `householdCount` 只算 1 户,`vehicleRowCount` 算 2 行 |
|
||||
| 批量确认里混入状态不对的户 | 整批仍按 `code:200` 返回,该户进 `failed[]` |
|
||||
| 120s 内同批重复提交 | `code:100502`,整批拒 |
|
||||
| 团期未定结束日 | `endDate: null` |
|
||||
| 车型字典为空 | 保存被拒(fail-closed),不退化成不校验 |
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `households[].status`(户级用车需求状态)
|
||||
|
||||
`PENDING_REVIEW`(待审核) / `PENDING` / `PROCESSING` / `DONE`,以及 **`null` = 该户一份都没提交**。`statusName` 由后端下发,**禁前端自映射**。
|
||||
|
||||
### `vehicleType`(车型大类编码)
|
||||
|
||||
**运行期字典,不是固定枚举**,取自 `fleet_vehicle_type`,通过 `GET /admin/fleet/vehicle-types/list` 下发。2026-09-22 测试服上存活 4 项(`suv2` / `mpv` / `sedan` / `bus`),但这是**当日快照、不是契约**——字典行可被增删下线,前端不得据此硬编码。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 接口 | 字段 | 改前 | 改后 |
|
||||
|---|---|---|---|
|
||||
| hotel / vehicle-households | `orderNo` 的 Swagger 说明 | 「子订单团号」,example `GT-26-0081` | 「子订单编号(非团号;形如 HL + yyyyMMddHHmmssSSS)」,example `HL20260516143052999`。**字段值本身从未变过**,一直是订单号 |
|
||||
| hotel / vehicle-households | `teamNo` | 不存在 | 新增,真团号,可为 null |
|
||||
| hotel / vehicle-households | `endDate` | 不存在 | 新增,可为 null |
|
||||
| vehicle-households | 户级 `status` / `statusName` | 不存在 | 新增(用房侧本来就有;`requirements[].status` 也早就有,别混淆) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 没提交用车需求的户 | **不在车侧响应里** | 在响应里,空卡 |
|
||||
| `householdCount` | 有活跃需求行的户数 | `households` 长度 |
|
||||
| `vehicleRowCount` vs `householdCount` | 恒 ≥ | **可能 <** |
|
||||
| 接送机确认 | 只能逐户 `dispatch?kind=TRANSFER` | 可批量,允许部分成功 |
|
||||
| 提交非法车型 | 通过,落库 | `809119` 拒 |
|
||||
|
||||
### 🔴 hl-ui 里已经不准确的 JSDoc(`origin/v2.1`,`src/api/orderV2GroupBatch.js`)
|
||||
|
||||
两个函数的 JSDoc 都写于改前,各有过期处。行号为 2026-09-22 在 `origin/v2.1` 上实读:
|
||||
|
||||
**`getGroupVehicleHouseholds`(JSDoc `:597-622`)**
|
||||
|
||||
- **`:600`**「被打回的需求行不在列表内(失活即消失,勿按用房那套找打回户)」——这句本身仍成立,但它隐含的「列表里的户都提交过」已不成立:未提交户现在也在列表里,且**和被打回户长得一模一样**。
|
||||
- **`:601-602`**「householdCount 按 orderId 去重…vehicleRowCount 数行,两者刻意不等价——表头「共 N 户」必须取 householdCount,禁取行数」——**结论仍然对**(表头就该取 `householdCount`),但它没说方向,读的人会默认行数 ≥ 户数,**改后可能反过来**。
|
||||
- **`:607-620`** 的 `@returns` 结构体——缺 `endDate`、`teamNo`、户级 `status`、户级 `statusName` 四个新字段。注意 `:613` 已有的 `status/statusName` 是 `requirements[]` 里的,不是户级的。
|
||||
|
||||
**`getGroupHotelHouseholds`(JSDoc `:568-589`)**
|
||||
|
||||
- **`:576-586`** 的 `@returns` 结构体——缺 `endDate` 与 `teamNo`;`:579` 列的 `orderNo` 需要补一句「不是团号」。
|
||||
- `:571-574` 关于打回户、未提交户与「汇总 == Σ 子订单」的几条口径**未变**,不必动。
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 面 | 评估 |
|
||||
|---|---|
|
||||
| 破坏性 | **无字段删除、无字段改名、无类型变更**,`orderNo` 的值与形态都没动 ⇒ 前端不改也不会报错 |
|
||||
| 但会静默出错的地方 | ①「共 N 户」若不是取 `householdCount` 而是别的推算,数字会和列表对不上;②「行数 ≥ 户数」的断言会在有未提交户时挂掉;③ 批量确认只看 `success` 会把部分失败当成全成功 |
|
||||
| 需要前端动手的 | 团号列改读 `teamNo`、空卡的展示与催提交入口、批量确认按钮与部分失败明细、车型下拉改走字典接口、`endDate` 展示 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- `GET /v3/admin/order/group-batch/{id}/orders`:未改,`teamNo` 本来就有,本次是让另外两个接口与它对齐。
|
||||
- 用房侧的列表口径与「汇总 == Σ 子订单」硬约束:未改。
|
||||
- 逐户 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch`:未改,仍可用。
|
||||
- 车侧汇总 `requirement-summary` 的既有口径:未改,仍只统计行程用车(#8151 的语义保持)。
|
||||
- 小程序端、`/mp/` 接口:零改动。
|
||||
- 网关:零新增路由。
|
||||
- 结算侧 `settlement/**`:与本次改动零文件重叠。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
网关 `https://api.test.1814.love:9443`,order-v3 读数 `dev-v3 / 62449e550 / 2026-09-22 22:22:58 / STATE=ok`(`62449e550` 即本单合并提交本身)。
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|---|---|
|
||||
| `teamNo` 三方同源 | hotel / vehicle / orders 三个接口逐户逐字符相等;已付订金户非空、未付订金户为 `null`,两种情形都覆盖 |
|
||||
| `teamNo ≠ orderNo` | 逐户为不同值 |
|
||||
| 未提交户进列表 | 空卡与 `status="PENDING_REVIEW"` 的已提交户在**同一份响应**里同时存在,字段有分辨力 |
|
||||
| 三个计数可区分 | 基线 `5 / 0 / 2`,终态 `6 / 0 / 3`,均两两不等 |
|
||||
| 批量确认 | 2 户全成功;DB 回读两户双表均 `PENDING_REVIEW → PENDING` |
|
||||
| 部分成功 | 1 成 1 败,失败户带 `orderId + errorCode(582083) + reason` |
|
||||
| 幂等 | 同批 120s 内第二次 `code:100502`;DB 回读无重复行 |
|
||||
| 车型字典两个方向 | 非法 `minivan` → `809119` 拒;合法 `bus` → 200 通过并回显 `vehicleTypeName` |
|
||||
| `endDate` | hotel / vehicle / batch-detail 三方均 `2026-11-21`,逐字符相等 |
|
||||
| 单测 / ArchTest | 定向 9 个类共 173 个用例逐类点名核对全绿,含 `MapperBoundaryArchTest` 27 与 `RedLineArchTest` 12 |
|
||||
|
||||
原始报文全部落盘(`D:/work2/_scratch/8195/ac/*.json`)。夹具为自建团期 `2102383303678189570` + 自建产品班期 `2102383185755312131`(`batchName` 为「8195夹具-2026年11月团期」),未复用他人夹具。
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #8151(团期子订单用车需求记录只读接口,本次改的就是它)
|
||||
- #7441(团期正式用车需求的存 / 读 / 撤回 / 免车四端点)
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Gitea Issue #8195
|
||||
- PR #8204(squash `62449e550`)
|
||||
- 相邻单 #8193(团期房务三事件站内信配置,同批部署)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- 工单: `https://git.1814.love:8443/wx/HL/issues/8195`
|
||||
- PR: `https://git.1814.love:8443/wx/HL/pulls/8204`
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: wx
|
||||
- 前端: mmg(hl-ui,分支 `v2.1`)
|
||||
@@ -7,14 +7,14 @@ author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "pending"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
frontend_ref: "91d476c9324f032f8160df73f937d6dfad69780a"
|
||||
target_release: "v2.1"
|
||||
verified_at: ""
|
||||
verified_at: "2026-09-22"
|
||||
updated_at: "2026-09-22"
|
||||
base: dev-v3
|
||||
status_note: "2026-09-22 wx 在「团期详情 → 查看需求」页逐屏截图提了 8 个问题。对 origin/dev-v3(cf9dc85a4) 与 hl-ui origin/v2.1(590c1155) 逐项查证后拆成两边:本条只收录后端零改动、今天就能动手的 5 项;另 3 项(另 5 处团号渲染点需后端补 teamNo、车侧未提交户后端根本不返回、接送机批量确认端点不存在)后端有硬缺口,已建 wx/HL#8195 处理,本条不含。查证依据:①名单表/打回弹窗/乘车户下拉三处的数据源 GB-ADM-003 GET /v3/admin/order/group-batch/{groupBatchId}/orders 已返回 teamNo(GroupBatchOrderItemRespVO.java:34,格式 26-0001,未付订金为 null);②逐户接送机确认可直接调已上线端点 POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER(VehicleRequirementAdminController.java:77,请求体 DispatchReqVO 只有 dispatchRemark);③GroupVehicleRequirementEditModal.vue:434 新建时 form.groups 恒为空数组、:407-408 两个日期恒为 null,blankGroup() :401-416 只被「添加分组」按钮调用;④GroupVehicleRequirementSection.vue:19 文案「新建正式需求」。UI 库是 Naive UI 不是 Element。"
|
||||
status_note: "2026-09-22 wx 在「团期详情 → 查看需求」页逐屏截图提了 8 个问题。对 origin/dev-v3(cf9dc85a4) 与 hl-ui origin/v2.1(590c1155) 逐项查证后拆成两边:本条只收录后端零改动、今天就能动手的 5 项;另 3 项(另 5 处团号渲染点需后端补 teamNo、车侧未提交户后端根本不返回、接送机批量确认端点不存在)后端有硬缺口,已建 wx/HL#8195 处理,本条不含。查证依据:①名单表/打回弹窗/乘车户下拉三处的数据源 GB-ADM-003 GET /v3/admin/order/group-batch/{groupBatchId}/orders 已返回 teamNo(GroupBatchOrderItemRespVO.java:34,格式 26-0001,未付订金为 null);②逐户接送机确认可直接调已上线端点 POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER(VehicleRequirementAdminController.java:77,请求体 DispatchReqVO 只有 dispatchRemark);③GroupVehicleRequirementEditModal.vue:434 新建时 form.groups 恒为空数组、:407-408 两个日期恒为 null,blankGroup() :401-416 只被「添加分组」按钮调用;④GroupVehicleRequirementSection.vue:19 文案「新建正式需求」。UI 库是 Naive UI 不是 Element。 | 2026-09-22 mmg 交付:团号上行(teamNo,null 显 — 勿兜底 orderNo)+删独立团号列;TRANSFER 待审核行逐行「确认」显式传 kind=TRANSFER;新建默认一空白分组+默认日期取团期 departDate/endDate(预填展开逐日行);按钮文案改「新增正式行程用车需求」;另 3 项硬缺口归 wx/HL#8195 不含本条"
|
||||
---
|
||||
|
||||
# 查看需求页:订单号改显团号 + 接送机逐行确认 + 正式用车需求弹窗三处默认值与文案
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8123"
|
||||
title: "团期 staff 保存新增两个错误码:并发抢锁 100503 与配置位字典缺失 582117"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "工单 #8123 修的是团期 staff 扇出并发丢配置:两个人几乎同时保存同一团期时,两条扇出各自读到自己那一刻的整期配置、互相覆盖,结果是子订单侧留下重复行而接口两次都返 200。修法是在 saveConfig 上加订单级 @Lock4j(锁名 batch-staff:save:{productBatchId},expire 30s,acquireTimeout 不显式设 = lock4j 默认 3s)并在锁内复读整期最新配置。由此对外多出一个此前这个端点不会返回的码 100503「资源被占用,请稍后重试」——它由 hl-common/hl-starter-protection 的 lockFailureStrategy 抛出,走 HTTP 200 不是 5xx。工单 #8148 把角色↔人员类型的判据从写死角色表改为读配置位字典(GroupBatchStaffSlotResolver#slotTypesOf),随之必须区分两种「查不到归属位」:DRIVER / OTHER 这类结构性不参与配置位的照旧放行,而本该有位却查不到的(运维把 GUIDE / LEADER / PHOTOGRAPHER 从某个仍非空的配置位字典里删掉)必须拒绝,否则该角色的 582114 会当场静默失效、任意人员类型都能落库并返 200,一直到结算对账才发现人岗对不上。这一支落新码 582117。生产代码由 PR #8177 合入 dev-v3(提交 bfc10966e),后续订正 PR #8186 / #8197 / #8199,取证载体 PR #8243。2026-09-23 测试服活体已确认字节就位:order-v3 双实例(8086 / 8186,jar 与磁盘同 inode)的进程字节里 STAFF_ROLE_SLOT_MISSING、RESOURCE_LOCKED、LockFailureExceptionHandler、LockFailureStrategy、slotTypesOf、GroupBatchStaffLockNames、participatesInSlotsByFallback 全部命中。acquireTimeout 的运行时值为默认 3000ms,判据是四个配置来源全零命中且各自带阳性对照:Nacos hl-order-service-v3-dev.yml(1256 字节,阳性对照 spring / datasource 各命中)、jar 内 BOOT-INF/classes/application.yml(6064 字节,阳性对照 spring 命中 4)、仓库 src/main/resources、两个进程的 JVM 启动参数与环境变量(阳性对照:每个进程 16 条启动参数);lock4j jar 版本 2.2.7,与源码 Lock4jProperties#acquireTimeout 默认值 3000L 同版本。前端侧:5821xx 这一族在 hl-ui v2.1 上没有专门分支,统一由 src/utils/request.js 的响应拦截器按 code 非成功值 toast 后端 message,故 100503 与 582117 前端零改动即有基本行为;记 pending 的是 100503 的语义——它是「稍后重试」而不是「操作失败」,用通用错误样式提示会把一次可重试的排队说成失败。前端结论(2026-09-23 用户拍板):100503 不做专门「可重试」提示,维持拦截器通用 toast;582117 文案后端完整直出,5821xx 全族前端无专门分支,基本行为零改动,记 not_required 闭环。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置: 保存口新增并发抢锁 100503 与配置位字典缺失 582117(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8177(主体)、#8186 / #8197 / #8199(订正)、#8243(取证载体)
|
||||
> **Issue**: #8123、#8148
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情页的「配导游 / 配摄影」弹窗保存动作
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
这个端点**多了两个此前不会返回的业务码**,路径、入参结构、成功响应结构一律不变。
|
||||
|
||||
1. **`100503`「资源被占用,请稍后重试」** —— 同一团期被两个人几乎同时保存时,后到的那一方等满 3 秒仍抢不到锁就收到它。**它是 HTTP 200,不是 5xx**,语义是「排队没排上,重来一次就好」,不是「你这次提交有问题」。
|
||||
2. **`582117`「角色 {0} 未归属任何人员配置位……」** —— 配置位字典被误删成员时的守卫。修复动作在**字典页面**,不在这个弹窗里,所以文案要原样透出给运营看。
|
||||
|
||||
两个码都会被 `src/utils/request.js` 的现有拦截器按「code 非成功值」自动 toast 后端 message,**前端零改动即有基本行为**;需要动手的只有 100503 的提示语气(详见「四、契约约束与正确调用方式」)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
### #8123:并发保存互相覆盖
|
||||
|
||||
保存团期 staff 的动作分两段——先写团期侧配置(`order_batch_staff`),再扇出到各活跃子订单(`order_staff_assignment`)。改前这两段都没有锁,于是两个人几乎同时保存同一团期时,两条扇出各自读到**自己那一刻**的整期配置并写下去,后写的不知道前面那份已经变了。子订单侧的结果是同一个人留下重复行,而两次请求**都返回 200**——调用方拿不到任何异常信号。
|
||||
|
||||
修法是在保存口加订单级分布式锁,并在锁内**复读整期最新配置**再扇出:
|
||||
|
||||
| 锁 | 锁名 | key 维度 | expire | acquireTimeout |
|
||||
|---|---|---|---|---|
|
||||
| 保存口 | `GroupBatchStaffLockNames.SAVE` | `batch-staff:save:{productBatchId}` | 30s | **默认 3s**(不显式设) |
|
||||
| 扇出 | `GroupBatchStaffLockNames.FAN_OUT` | 同团期 | — | 显式拉长(异步后台任务,宁可等久也不能丢单) |
|
||||
|
||||
两者的等待上限**刻意不一致**:保存口背后有个人在等页面响应,让他 3 秒后重试是对的(他重新打开还能看见别人刚存的最新配置);扇出没有人在等返回,放弃的代价是某张子订单**永久**停在上一轮配置。
|
||||
|
||||
### #8148:角色判据从写死表改为读字典
|
||||
|
||||
角色↔人员类型的校验(`582114`)改前查的是代码里写死的角色表,改后读配置位字典(`GroupBatchStaffSlotResolver#slotTypesOf`)。这让业务给导游位字典加一行新角色时当天就能生效、不必发版,但也带来一个新情形:
|
||||
|
||||
| 「查不到归属位」的成因 | 改前含义 | 改后处置 |
|
||||
|---|---|---|
|
||||
| `DRIVER` / `OTHER` 等结构性不参与配置位的角色 | 唯一含义,不校验是对的 | **照旧放行** |
|
||||
| 运维把 `GUIDE` / `LEADER` / `PHOTOGRAPHER` 从某个仍非空的配置位字典里删掉 | 不存在这种情形 | **拒绝,返 582117** |
|
||||
|
||||
两种情形若继续共用「不校验」这一条出路,那一刻该角色的 `582114` 会**当场静默失效**:任意人员类型都能配成这个角色落库、接口返回 200,一直到结算对账才发现人岗对不上。两支由 `participatesInSlotsByFallback` 分开。
|
||||
|
||||
**它不是「字典读挂」的兜底**:Feign 失败或字典整表为空时 `typesOf` 回落内置默认值,归属位照样查得到,走不到这个码。能触发它的只有「字典非空、但该角色被从里面删掉」。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改接口 | 新增可能返回的业务码 `100503`、`582117`;路径 / 入参 / 成功响应结构不变 |
|
||||
|
||||
路径、入参结构、权限码、成功响应结构均零变化;网关无改动。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期人员配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「配导游 / 配摄影」弹窗点保存。整期全量覆盖:按 `scopeRoles`(不传即全量)软删旧配置、写入新配置、
|
||||
异步扇出到各活跃子订单,并按配置结果回填 `guide_ready` / `photographer_ready`。
|
||||
|
||||
🔴 **路径参数是产品侧排期 ID,不是团期聚合主键**。端点名叫 `group-batch`,但 `{productBatchId}` 吃的是
|
||||
`group_tour_batch.batch_id`(`@ApiParam` 原文:「产品侧排期 ID(`group_tour_batch.batch_id`,非运营团期主键)」)。
|
||||
响应体里另有一个 `groupBatchId`,那才是订单侧 `order_group_batch` 的主键,两者是不同的值,不要互换。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 正整数,产品侧排期 ID | 团期所属班期;未建团返 589553 |
|
||||
| scopeRoles | body | List<String> | 否 | 传了就不能是空数组;元素非空白且须在员工角色取值域内 | 限定本次覆盖的角色范围,不传则整期覆盖 |
|
||||
| staffList | body | List<Item> | 否 | null 按空列表处理 | 传空列表 = 清空覆盖范围内的配置 |
|
||||
| staffList[].staffId | body | Long | 是 | 须命中候选人员 | 人员 ID |
|
||||
| staffList[].staffRole | body | String | 是 | 须与人员类型相符,否则 582114 | 角色(GUIDE / LEADER / PHOTOGRAPHER …) |
|
||||
| staffList[].sortOrder | body | Integer | 否 | — | 展示排序 |
|
||||
| staffList[].remark | body | String | 否 | — | 备注 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| productBatchId | Long | 回显路径参数 |
|
||||
| groupBatchId | Long | 团期聚合主键,与路径参数不是同一个值 |
|
||||
| staffList | List | 保存后的整期最终状态 |
|
||||
| affectedOrderCount | Integer | 本次扇出触及的活跃子订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/group-batch/2102640646949105666/staff HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"staffList":[{"staffId":1002,"staffRole":"GUIDE","sortOrder":0,"remark":"hl8123-8148live-normal"}]}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"productBatchId": "2102640646949105666",
|
||||
"groupBatchId": "2102640736900116481",
|
||||
"affectedOrderCount": 1,
|
||||
"staffList": [
|
||||
{"id": "2102640914386284545", "staffId": 1002, "staffRole": "GUIDE", "staffName": "李雪梅", "remark": "hl8123-8148live-normal"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`staffList` 传空列表即清空覆盖范围内的配置,返回 200,`staffList` 为空数组、`affectedOrderCount` 为实际扇出订单数。
|
||||
被任何一个业务码拒绝时都是**零写入**——校验闸全部在写库之前,回读会看到与提交前逐字相同的配置。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
同一团期并发保存,后到的一方等满 `acquireTimeout`(默认 3000ms)仍未抢到锁:
|
||||
|
||||
```json
|
||||
{"code": 100503, "message": "资源被占用,请稍后重试", "data": null}
|
||||
```
|
||||
|
||||
配置位字典里查不到某个本该有位的角色的归属位(`{0}` 由运行期填入角色名):
|
||||
|
||||
```json
|
||||
{"code": 582117, "message": "角色 GUIDE 未归属任何人员配置位,无法校验人员类型;请检查配置位字典是否被误删", "data": null}
|
||||
```
|
||||
|
||||
本端点此前已有、本次不变的两个高频码,一并列出便于对照:
|
||||
|
||||
```json
|
||||
{"code": 582116, "message": "本次保存声明的角色范围(GUIDE)只覆盖了配置位的一部分,还缺少 LEADER;同一配置位的角色必须一起声明,否则位内其余人员会被留在库里", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `100503` 的锁维度是 **`productBatchId`**:并发窗口只存在于**同一个团期**的两次保存之间,不同团期互不阻塞,整体串行化的担心不成立
|
||||
- `100503` 走 **HTTP 200**,不是 5xx;它有两条产生路径(`lockFailureStrategy` 直接抛 `BusinessException`,或异常以 `LockException` 形态漏出时由 `LockFailureExceptionHandler` 兜底),**终点是同一个码**,调用方不必区分
|
||||
- `582117` 的修复动作在**配置位字典页面**,不在本弹窗;文案点名了角色并指向字典,适合原样透出给运营
|
||||
- `582117` **不会**在「字典读挂 / 字典整表为空」时出现——那种情形回落内置默认值,归属位照样查得到
|
||||
- `582115`(提交的人越界)与 `582116`(声明的范围本身不完整)分工不同:前者改 `staffList` 或扩范围,后者只有一条改法——把缺的同位角色补进 `scopeRoles`。命中 582116 的请求可以**一个越界的人都没有**(`staffList` 甚至可以是空数组,那正是「清空导游位」这条最危险的路径)
|
||||
- 导游位并收 `GUIDE` 与 `LEADER`:只传 `["GUIDE"]` 却选了领队会命中 582115,只传 `["GUIDE"]` 而位内还有 LEADER 会命中 582116
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **`100503` 要单独做提示,不要并进通用错误 toast**。现有拦截器按 `code` 非成功值统一 toast 后端 message,
|
||||
行为上没有错,但通用错误样式会把一次「排队没排上、再点一次就好」说成「操作失败」。建议:命中 100503 时
|
||||
保留用户已填的表单、给一句可重试的提示(后端文案「资源被占用,请稍后重试」本身已是可直接展示的完整句子)。
|
||||
- **判成败一律看 `code`,不要看 HTTP status**。本端点的业务失败(含 100503)全是 HTTP 200,
|
||||
`src/utils/request.js:413` 现有的 `code === BIZ_CODE.SUCCESS` 判据是对的,沿用即可。
|
||||
⚠️ fleet `h5-onboard` 那条独立 axios 实例上有一处 100503 的处理走的是 HTTP 409 语境,**两条链路不同源,不要互相复用**。
|
||||
- **`productBatchId` 与响应里的 `groupBatchId` 不可互换**(见「使用场景」的红字)。
|
||||
- 保存成功后若调用方缓存了 `guide_ready` / `photographer_ready`,需重新拉取团期详情。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 保存:按 `scopeRoles` 范围(不传即整期)软删旧配置行 → 写入新配置行 → 异步扇出到各活跃子订单 → 回填团期行的两个 ready 标志
|
||||
- 本次新增的锁不改变上述任何一步的写内容,只保证同一团期的两次保存不会交叠执行,且扇出读到的是锁内复读的最新配置
|
||||
- 被 `100503` / `582114` / `582115` / `582116` / `582117` 任一码拒绝时**零写入**
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `100503` 的等待上限 3000ms 来自 lock4j 默认值(运行时无任何显式覆盖),不是代码里写死的常量;改动保存口的锁参数等于改这条对外契约,已由单测 `GroupBatchStaffFanOutLockRetryTest#lockParameters_saveUsesLock4jDefaultAcquireTimeout_whileFanOutOverridesItToOutliveExpire` 钉成可执行断言
|
||||
- `582117` 与 `582114` 是互补而非替代:字典里查得到归属位、但人岗不符 → `582114`;本该有位却查不到归属位 → `582117`
|
||||
- `DRIVER` / `OTHER` 这类结构性不参与配置位的角色在两个码上都放行,行为与改前一致
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 同一团期并发保存 | 两条扇出互相覆盖,子订单侧留重复行,**两次都返 200** | 后到者等满 3s 返 `100503`;抢到锁者在锁内复读最新配置再扇出 |
|
||||
| 角色↔人员类型判据 | 代码里写死的角色表 | 读配置位字典(字典加角色当天生效、不发版) |
|
||||
| 配置位字典被删成员 | 该角色的 `582114` **静默失效**,任意人员类型都能落库并返 200 | 返 `582117`,拒绝落库 |
|
||||
| `DRIVER` / `OTHER` | 放行 | **不变**,仍放行 |
|
||||
| 路径 / 入参 / 成功响应结构 | — | **零变化** |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 面 | 评估 |
|
||||
|---|---|
|
||||
| 管理后台配导摄弹窗 | 现有拦截器已能 toast 两个新码的后端文案,**零改动即有基本行为**;需要动的只有 100503 的提示语气与表单保留 |
|
||||
| 已有错误文案分支 | 5821xx 在 hl-ui v2.1 上无专门分支(全族零命中),统一走拦截器,无需逐码适配 |
|
||||
| 小程序端 | 不受影响,团期人员配置无小程序写口 |
|
||||
| 并发体验 | 只在同一团期被两人同时编辑时才可能出现 100503;不同团期互不阻塞 |
|
||||
| 运维 | 配置位字典删成员会立刻让该位保存被 582117 拒——这是有意的,文案指向字典页面 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 路径、入参结构、权限码、成功响应结构、网关配置
|
||||
- 候选人查询(`/staff/candidates`、`/staff/candidates/page`)与配置列表查询(`GET /staff`)
|
||||
- 报账人等级设置口 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`:本次未取证,不在本条目覆盖范围内
|
||||
- 子订单自己单独派的人员(`order_staff_assignment` 中 source 非 `GROUP_BATCH` 的行)
|
||||
- `582114` / `582115` / `582116` 三个既有码的触发条件与文案
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
2026-09-23,`api.test.1814.love:9443`,分支 `dev-v3`,order-v3 活体 `7738668b8`(双实例 8086 / 8186,进程 jar 与磁盘 jar 同 inode)。
|
||||
|
||||
| 项 | 请求 | 结果 |
|
||||
|---|---|---|
|
||||
| 正常路径回归 | `PUT /v3/admin/group-batch/2102640646949105666/staff`,body 不传 `scopeRoles`(整期全量覆盖),一条 `{staffId:1002, staffRole:"GUIDE"}` | HTTP 200,`code=200`,`message=成功`;`data.staffList` 回显该行,`affectedOrderCount=1` |
|
||||
| 落库回读 | `GET /v3/admin/group-batch/2102640646949105666/staff` | HTTP 200,返回同一行(`id=2102640914386284545`),**确认真落库、不是空操作假成功** |
|
||||
| 582116 | 同端点,`{"scopeRoles":["GUIDE"], staffList:[GUIDE 一人]}`(漏声明同位的 LEADER) | HTTP 200,`code=582116`,文案完整返回;回读确认**零写入**(记录仍是上一步那行) |
|
||||
| 582114 | 同端点,`staffId=1002`(真实 `staffType=GUIDE`)提交 `staffRole=PHOTOGRAPHER` | HTTP 200,`code=582114`,`message=所选人员的角色与其人员类型不符,请重新选择`;回读确认**零写入** |
|
||||
|
||||
**这一轮没有取到 `100503` 与 `582117` 的活体样本,两者在测试环境上无法构造**,原因是环境性的、与实现无关:
|
||||
|
||||
- `100503` 要求持锁方占用临界区超过 `acquireTimeout`=3000ms 才能让后到者抢不到。真锁双臂单测里实测的等锁只有 **373ms**,
|
||||
而把临界区人为拉长到 3 秒以上需要改代码或插延迟——那会改变被测对象本身。
|
||||
- `582117` 要求把 `GUIDE` / `LEADER` / `PHOTOGRAPHER` 从某个**仍非空**的配置位字典里删掉。字典是全站共享数据,
|
||||
这个动作的副作用会落到所有人的角色下拉框上。
|
||||
|
||||
因此这两个码本条目交付的是**契约**(码值、HTTP 状态、文案、触发条件、锁维度),依据是源码定义与测试服活体字节确认,
|
||||
不是端到端样本。它们的行为另有可执行断言背书:`100503` 由真 lock4j AOP + 真 Redis 的双臂互斥测试覆盖(无锁臂 4 行重复
|
||||
vs 真锁臂恰好 2 行,后进者等锁 373ms,且断言链最后一条是**两臂之差**,「这一轮恰好没撞上竞争」过不了关);
|
||||
`582117` 由源码变异实验覆盖(把读字典改回写死表、把 582117 分流短路掉,两轮各有用例转红,方向互补)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8123(团期 staff 扇出并发写重复行)、Issue #8148(配置位成员集合改读字典)
|
||||
- PR #8177(主体,提交 `bfc10966e`)、PR #8186 / #8197 / #8199(订正)、PR #8243(取证载体)
|
||||
- 错误码定义:`hl-order-service-v3/.../assignment/errorcode/AssignmentErrorCode.java`(582115 / 582116 / 582117)、
|
||||
`hl-common/hl-common-core/.../errorcode/CommonErrorCode.java:67-68`(100503)
|
||||
- 后续工单 #8242:三个合法 staffRole 继承了 DRIVER 的类型校验豁免(与 582117 是不同的洞)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:wx
|
||||
- 前端:mmg(100503 的可重试提示语气与表单保留)
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8142"
|
||||
title: "车务派单看板: 行程用车与接送机需求并存时,接送机需求整行从列表消失——已修复,同一订单现按需求维度出多条记录"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修复"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "本条不改变 GET /admin/fleet/board/orders 的任何响应字段名、类型或嵌套结构,变的是记录的产出规则:修复前该端点在「一单同时有活跃 TRAVEL 需求与活跃 TRANSFER 需求」时只产出其中一条需求的记录,另一条整行不出现(且出现的那条还可能挂着另一类的 requirementRemark 与派车行);修复后每条活跃用车需求各出一条记录。gateway_status=not_required:零新增路由,端点既有。frontend_status=not_required:hl-ui(origin/v2.1)的看板列表与卡片视图早已用 requirementId 作 row-key(src/views/fleet/board/utils/boardSummary.js:9-14,resolveBoardRecordKey,注释原文「看板已按当前有效用车需求聚合;列表身份必须锚定 requirementId,历史响应缺失时才回退原记录/订单 ID」),同一订单多条记录不会撞 key,无需前端改动——这是查证结论不是推断,若后续发现有按 orderId 去重的调用点,以那处为准另发订正。backend_status=deployed:hl-fleet-service 与 hl-order-service-v3 均已滚动到测试服 78d0aad3d(分别 2026-09-23 07:38:54 / 07:40:25),并在测试服活体上完成改前/改后对跑取证,读数见正文第四节。 mmg 2026-09-23 复核: grep 实证 resolveBoardRecordKey 锚定 requirementId(boardSummary.js:9-14)多单不撞 key;virtualPending 为既有字段,index.vue/display.js/AssignModal 均已消费;域内唯一按 orderId 建的 Map 是未读数查找表(unreadByOrderId),多单同值无冲突——not_required 成立。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车务派单看板: 行程用车与接送机需求并存时,接送机需求整行从列表消失
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service(看板候选与聚合)+ hl-order-service-v3(需求身份供给)+ hl-common
|
||||
> **PR**: [#8211](https://git.1814.love:8443/wx/HL/pulls/8211)
|
||||
> **Issue**: [#8142](https://git.1814.love:8443/wx/HL/issues/8142)
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: `GET /admin/fleet/board/orders`(车务派单看板列表 / 待派分面)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **同一订单现在可能出现多条记录,每条活跃用车需求各一条。** 字段名、类型、嵌套结构**零变化**,变的是**记录的产出规则**。
|
||||
如果你的代码里有「一个 orderId 对应一条记录」这个隐含假设(按 orderId 去重、按 orderId 建 Map、用 orderId 当列表 key),**它现在不成立了**。
|
||||
`hl-ui` 当前实现不受影响——看板列表与卡片视图已用 `requirementId` 作 row-key(`src/views/fleet/board/utils/boardSummary.js:9-14`)。
|
||||
|
||||
2. **`requirementId` 现在是列表记录的身份键,`orderId` 不是。** 唯一性口径是 `(orderId, requirementId)` 组合,`requirementId` 恒非 null。
|
||||
|
||||
3. **`requirementRemark` / `dailyAssignments` / `requiredVehicles` 现在保证属于本记录自己的那条需求。** 修复前存在反向错配:TRANSFER 需求的记录上挂着 TRAVEL 需求的 `requirementRemark`,派车行也只显示了其中一部分。
|
||||
|
||||
4. **「接送机还没派车」的虚拟待派卡现在会出现在待派分面里**(`virtualPending: true`、`dailyAssignments: []`、`assignmentStatus: "unassigned"`、`assignmentGroupId: null`)。修复前这类需求在列表侧完全不可见,车务无从发现「这单的接送机还没派车」。
|
||||
|
||||
---
|
||||
|
||||
## 一、变更接口清单
|
||||
|
||||
| 方法 | 路径 | 变更性质 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/fleet/board/orders` | 响应**字段结构不变**,记录产出规则变更(一单一条 → 一需求一条) |
|
||||
|
||||
> 本条不属「新增接口 / 修改接口」:无新增或删除的请求参数,无新增、删除或改名的响应字段,无新增错误码。
|
||||
|
||||
---
|
||||
|
||||
## 二、失败形态(供你判断历史数据与截图)
|
||||
|
||||
修复前有两种形态,都**静默**——接口返 200、`code=200`、无任何错误码或警告字段:
|
||||
|
||||
**形态甲 · 整批不出现**:两类需求并存时只产出一条记录,另一条整行没有。
|
||||
实测:某广口径查询 `.data.total` 修复前 **22**、修复后 **25**,多出的 3 条恰好一单一条落在三个订单上。
|
||||
|
||||
**形态乙 · 反向错配**:产出的那条记录 `requirementId` 是 A 需求的,而 `requirementRemark` 是 B 需求的,`dailyAssignments` 只含 A 的一部分派车行。
|
||||
实测订单 `HL20260919104049371`:修复前该条记录 `requirementId=7330964160969957`(TRANSFER)却带着 TRAVEL 的备注、只有 1 条日行;修复后备注是自己的、日行 2 条(与库中该需求的活派车行集合逐条相等)。
|
||||
|
||||
⇒ **修复前的看板截图与导出数据不可作为历史对账依据**,涉及两类需求并存的订单需重新拉取。
|
||||
|
||||
---
|
||||
|
||||
## 三、边界:有一类记录按设计仍然不出现
|
||||
|
||||
用车需求处于 `DONE`/`CANCELED` 等**非待派状态**、且**零条活派车行**时,看板**不为它造虚拟待派卡**,只打一条 warn 日志。
|
||||
这属于数据不一致的兜底分支(`BoardCandidateSource.java:383-397`,判据 `isAwaitingDispatch = PENDING || PROCESSING`),**不是本次修复的残留**。
|
||||
⇒ 若你看到某单「库里有这条需求、看板上没有」,先看该需求的 `status`:非 `PENDING`/`PROCESSING` 且零派车行时,这是预期行为。
|
||||
|
||||
---
|
||||
|
||||
## 四、后端已验证的读数(测试服活体,部署 commit `78d0aad3d`)
|
||||
|
||||
| 验证项 | 读数 |
|
||||
|---|---|
|
||||
| 虚拟待派卡可见 | 某 TRANSFER 需求记录数 `0 → 1`,该单 `.data.total` `1 → 2`;`virtualPending=true`、`dailyAssignments=[]` 与库一致 |
|
||||
| 两类派车行各自完整 | TRAVEL 与 TRANSFER 两条需求的日行 `assignmentGroupId` 集合,与库中各自的活派车行集合**逐条相等** |
|
||||
| 单类订单零回归 | TRAVEL-only 与 TRANSFER-only 两单,响应递归展平后各 243 / 205 个叶路径,**各只有 1 个键变化**且都是随日期自然漂移的 `daysUntilDeparture`(恰好 −1);剔除该键后改前/改后 sha256 **完全相等** |
|
||||
| 待派分面未被打穿 | 广口径 `?statuses=unassigned` 返 `total=133`;fleet 日志中 fail-open 分支关键词 **0 命中**(同窗口任意 board 日志行 21 条,证明日志在写) |
|
||||
| 记录唯一性 | 8 份响应逐份核 `(orderId, requirementId)` 对唯一、`requirementId` 非 null 全绿 |
|
||||
|
||||
---
|
||||
|
||||
## 五、你需要做的
|
||||
|
||||
- **无需改动**即可继续工作;若你的代码按 orderId 去重或建 Map,改为按 `requirementId`(或 `(orderId, requirementId)`)。
|
||||
- 渲染时优先用 `requirementId` 作列表 key。
|
||||
- 展示「这单还有没有没派的车」时,把 `virtualPending: true` 的记录算进去——它就是「有需求、还没派车」这一状态的载体。
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8166"
|
||||
title: "通知消息bizType路由与bizId语义契约交接"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "77b421f839a0eaeaa419c19bb6f79a803dd99205"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "关联工单#8166(bizType/bizId契约漂移)的前端交接件。后端已就绪,前端按本件调整三处routing逻辑。前端已交付(2026-09-23,三条必落地+AC-9 全做):①jumpBiz 改白名单兜底——订单白名单(ORDER/HOUSE/HOUSE_LEAD/GROUP/FLEET)才按 orderId 路由,新增/未知 bizType 不入列;②canJumpToBiz 白名单外返 false 不渲染跳转按钮,未知即不跳;③GROUP_HOUSE/GROUP_FLEET/GROUP_BATCH 按 bizId=groupBatchId 路由团期详情页 /order-v2/batch/detail/{id};AC-9 死分支(peer==CUSTOMIZER→housekeeper/orders,peerRole/senderRole/requirementId 后端从未返回)已删。路由解析抽 resolveBizRouteTarget 纯函数,spec 13 例全绿,checkpoint 全过。正文首版第一节把兜底分支位置写成 jumpBiz.js 的 else,已于 2026-09-23 订正为 index.vue:291-293,并补一节交付后必查(HOUSE NOTIFY 三义 bizId)。后端订正(补 HOUSE NOTIFY 复核项)前端已对照:resolveBizRouteTarget 中 isHouseNotifyRow 闸在白名单命中之前,HOUSE NOTIFY 行(bizId 三义)恒返 null,spec 已有该定向例({bizType:HOUSE,kind:NOTIFY,bizId 任意雪花}→null),未打穿 #8182;交付 commit 77b421f8 已可达 origin/v2.1(取证时因推送延迟暂不可见)。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 通知消息 bizType 路由与 bizId 语义契约交接
|
||||
|
||||
关联工单:[#8166](https://git.1814.love/wx/HL/issues/8166) bizType/bizId 契约漂移定案
|
||||
|
||||
> **2026-09-23 订正**:本件首版把兜底分支的位置写成了 `jumpBiz.js` 的 `else`。实测 `origin/v2.1` 的
|
||||
> `jumpBiz.js` 全文 44 行、**没有 `else`、也没有 bizType 白名单**,兜底在 `index.vue:291-293`。
|
||||
> 第一节已按实际代码重写,其余各节未变。前端已按正确形状交付(见 frontmatter),本节保留是因为首版那句错的描述已经发出去过,留着会让下一个读这份交接件的人以为 `jumpBiz.js` 里有一个并不存在的分支。
|
||||
|
||||
## 〇、交付后必查一项:`HOUSE` 进订单白名单会不会打穿 #8182
|
||||
|
||||
frontmatter 记的白名单是 `ORDER / HOUSE / HOUSE_LEAD / GROUP / FLEET` → 按 `orderId` 路由。**`HOUSE` 这一项只有在仍受 `isHouseNotifyRow` 约束时才是安全的。**
|
||||
|
||||
判据在 `jumpBiz.js` 自己的头部契约里(`origin/v2.1` = `16c3506d`,`jumpBiz.js:4-5`):
|
||||
|
||||
> HOUSE 域 bizId 三义(hotelId/groupBatchId/orderId)不是路由键,禁止拿它反推页面
|
||||
> - HOUSE 非聊天行且 link 不可用 → 不渲染入口也不跳(bizId 当 orderId 必开错单)
|
||||
|
||||
`isHouseNotifyRow`(`jumpBiz.js:36-38`)= `(bizModule || bizType) === 'HOUSE' && !isChatRow(row)`。⇒ **`bizType === 'HOUSE'` 这一个条件分不开两类行**:CHAT 行的 bizId 是 orderId(可路由),NOTIFY 行的 bizId 三义(不可路由)。白名单若只按 `bizType` 匹配、且 `isHouseNotifyRow` 那道闸在它之后或被一并删掉,HOUSE NOTIFY 行就会**按 orderId 打开一个错的订单**——而 `hotelId` / `groupBatchId` 也是雪花 id,**页面不会报错,只会显示另一张单**,这正是 #8182 修掉的那个形态。
|
||||
|
||||
**一句话自检**:`resolveBizRouteTarget` 对一个 `{ bizType: 'HOUSE', kind: 'NOTIFY', conversationKey: null, bizId: '<任意雪花id>', link: '' }` 的行,返回的必须是「不跳」,不是订单路由。spec 里若没有这一例,它就是白名单里唯一一个靠**行内第二个条件**才安全的成员,而那个条件不在白名单的表达式里。
|
||||
|
||||
**取证边界**:本节没有对交付提交本身取证——`77b421f839a0eaeaa419c19bb6f79a803dd99205` 在 `git ls-remote origin`(hl-ui)返回的 15 个 ref 里查无此对象,阳性对照同一命令能查到 `16c3506d`(`refs/heads/v2.1`)。上面的判据因此全部取自 `origin/v2.1` tip `16c3506d`,即交付**之前**的代码;白名单的实际写法请以你们手上那份为准。
|
||||
|
||||
## 一、兜底分支:bizId 被当成 orderId
|
||||
|
||||
### 代码现状(实测 `hl-ui` `origin/v2.1`)
|
||||
|
||||
跳转链路是**两个文件配合**的,改一处不够:
|
||||
|
||||
| 位置 | 职责 | 现状 |
|
||||
|---|---|---|
|
||||
| `src/views/notification/MyMessages/jumpBiz.js:40-43` `canJumpToBiz()` | 决定**渲不渲染**「跳转」按钮 | `link` 可用→true;无 `bizId`→false;否则 `!isHouseNotifyRow(row)`——**只挡 HOUSE NOTIFY 一类** |
|
||||
| `src/views/notification/MyMessages/index.vue:267-294` `jumpToBiz()` | 决定**跳去哪** | `link` 优先(`:268-272`)→`FLEET`(`:280`)→定制师房务分支(`:282`)→HOUSE NOTIFY 兜底闸(`:288-290`)→**`else`(`:291-293`) `getOrderReadableRoute(bizId)`** |
|
||||
|
||||
`else` 那一支把 `bizId` 无条件当作 **orderId**。凡是落到这一支、而 `bizId` 实际不是订单 ID 的消息,都会打开一个**错的订单详情页**——页面正常渲染,没有报错,用户不知道自己看的不是这条消息说的那个东西。
|
||||
|
||||
### 谁会落到这一支
|
||||
|
||||
`link` 为空的行才走推导。**CHAT 通道的行天然没有 `link`**(`link` 是 NOTIFY 按事件模板渲染出来的),所以团期级会话行 `GROUP_HOUSE` / `GROUP_FLEET` 会:
|
||||
`canJumpToBiz` 判 true(有 bizId、不是 HOUSE NOTIFY)⇒ 按钮渲染 ⇒ `jumpToBiz` 前四支都不命中 ⇒ 落 `else` ⇒ 拿 **groupBatchId** 去开订单详情页。
|
||||
|
||||
### 方向说明:不要退回「bizType 推路由」
|
||||
|
||||
`jumpBiz.js` 开头的契约(#8182 建立)写得很明确——**NOTIFY 行跳转的唯一正确来源是 `link`,HOUSE 域 bizId 三义不是路由键**。本件**不是**要推翻它回到按 bizType 推路由,而是给那条**残留的**推导加一道白名单闸:能走推导的只有已确认 `bizId == orderId` 的那几类,其余一律不跳。
|
||||
|
||||
## 二、前端改动(3 条,缺一不可)
|
||||
|
||||
1. **`index.vue:291-293` 的 `else` 加白名单**:只有 `mod` ∈ `{ORDER, HOUSE, HOUSE_LEAD, GROUP, FLEET}`(即第三节表里 bizId 为 orderId 的那几类)才 `getOrderReadableRoute(bizId)`;其余**直接 return,不跳**。
|
||||
|
||||
2. **`jumpBiz.js:40-43` 的 `canJumpToBiz` 同步收口**:把同一份白名单用上,否则会渲染出一个**点了没反应**的按钮——那比跳错更让人困惑。两处必须用同一份常量,不要各写一份。
|
||||
|
||||
3. **团期级 bizType 路由到团期详情页**:`GROUP_HOUSE` / `GROUP_FLEET` / `GROUP_BATCH` 的 `bizId` 是 `groupBatchId`,应路由到团期详情(`src/views/order-v2/batch/detail/`,页面已存在),不是订单详情。
|
||||
|
||||
**这三条落地后的行为变化**:以后后端新增一个 bizType 而前端没跟着改,后果从「**跳到错的订单页**」变成「**不跳**」。失败从隐形变成可见——这是本次改动真正买到的东西,不是多支持了几个跳转。
|
||||
|
||||
## 三、bizId 语义契约(原只活在注释里,本件升级为书面契约)
|
||||
|
||||
判据取自 `origin/dev-v3` 源码,不是推测:
|
||||
|
||||
| bizType | 通道 | `bizId` 实际值 | 出处 |
|
||||
|---|---|---|---|
|
||||
| `ORDER` | CHAT / NOTIFY | orderId | #8155 |
|
||||
| `HOUSE` | CHAT | orderId | `ChatManager.java` / `TeamChatModule.java` |
|
||||
| `HOUSE_LEAD` | CHAT | orderId | 同上 |
|
||||
| `FLEET` | CHAT | orderId | 同上 |
|
||||
| `GROUP` | CHAT | orderId | 同上 |
|
||||
| `GROUP_HOUSE` | CHAT | **groupBatchId** | 同上 |
|
||||
| `GROUP_FLEET` | CHAT | **groupBatchId** | 同上 |
|
||||
| `GROUP_BATCH` | **NOTIFY** | **groupBatchId** | `GroupBatchFormedNotifyService.java:209-210`(`.bizId(String.valueOf(groupBatchId))` + `.bizType("GROUP_BATCH")`) |
|
||||
|
||||
⚠️ `HOUSE` 这一行只覆盖 **CHAT** 行。**HOUSE 域的 NOTIFY 行 bizId 是三义的**(hotelId / groupBatchId / orderId),前端已有 `isHouseNotifyRow` 把它挡在推导之外(`jumpBiz.js:35-37`),本次白名单**不要**把 HOUSE NOTIFY 放进来。
|
||||
|
||||
## 四、`peerRole` / `senderRole` 不补(后端定案)
|
||||
|
||||
`AdminMessageRespVO` **不新增** `peerRole` / `senderRole`。
|
||||
|
||||
**判据**(实测 `origin/dev-v3`):
|
||||
- 该 VO 现有 **15 个字段**:`messageId` `categoryCode` `title` `content` `link` `bizId` `bizType` `isRead` `createTime` `messageType` `messageTypeLabel` `kind` `senderName` `conversationKey` `teamMessage`。`peerRole` / `senderRole` / `bizModule` / `requirementId` **四个都不在其中,也从未返回过**。
|
||||
- 定制师广播时 `admin_message.sender_role` **恒为 NULL**(`AdminMessageMapper.java:443-444` / `ChatConstants.java:200-201`)⇒ 就算把字段加上,在前端那条分支**正需要它**的场景里它恒为空。
|
||||
|
||||
**前端据此处理**:`index.vue:282-287` 的第三层分支
|
||||
`peer === 'CUSTOMIZER' && (mod === 'HOUSE' || mod === 'HOUSE_LEAD')` 取值自 `:279 row.peerRole || row.senderRole || ''`,两个来源都恒空 ⇒ **该分支恒不触发,是死代码,请删除**。删掉后 `HOUSE` / `HOUSE_LEAD` 的 CHAT 行按第二节的白名单走订单详情,与今天的实际行为一致。
|
||||
|
||||
若业务确实需要「房务会话行定向跳房务订单页」,那是一个新需求:它要的数据后端今天不产出,需要单独定接口。
|
||||
|
||||
## 五、覆盖边界
|
||||
|
||||
1. **bizType 清单是下界,不是全集**:`AdminNotificationController.java:127` 的 `.eventCode(config.getEventCode())` 是运行时入参,能发 `notification_event_config` 表里任意一行的事件码 ⇒ 任何静态枚举都数不全。这正是第二节要白名单兜底而不是黑名单的原因:**白名单对「没见过的类型」给出的是安全答案(不跳),黑名单给出的是错答案(跳错)**。
|
||||
2. **取证范围是 `origin/dev-v3`**。一期 `origin/dev` 的 `hl-user-service` 是另一份代码(渲染器与通道校验都不同),本件对一期的行为不作任何断言,不要把这里的结论套到一期页面上。
|
||||
3. **第三节的表只覆盖本件列出的 8 个 bizType**。表外的类型按第二节落到「不跳」,这是设计如此,不是遗漏。
|
||||
|
||||
## 六、关联
|
||||
|
||||
- 工单:[#8166](https://git.1814.love/wx/HL/issues/8166)
|
||||
- 后端联系人:@wx
|
||||
@@ -0,0 +1,628 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8170"
|
||||
title: "订单协作域三只读端点补订单归属守卫(新增返回 581008/581045);行程价格字段与团期批量打回契约补充说明"
|
||||
consumer: "admin"
|
||||
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: "PR #8290 已合并 dev-v3(841ea7b06)。部署:hl-order-service-v3 dev-v3 @ 841ea7b06,2026-09-23 20:07:37 部署,deploy-status STATE=ok,BEHIND 0。真实网关实测(夹具订单 2100542584106496001,归属定制师为他人):CUSTOMIZER(非归属)在 tags / tag-picker / itinerary-document 三个端点均返回 581008;SUPER_ADMIN(对照)三个端点均正常返回 200;itinerary-document 不带 documentType 时先因参数校验返回业务码 400,发生在归属守卫之前。GET /{id}/itinerary 与批量打回 resourceType 两处仅补充字段说明文案,接口行为、请求/响应结构均未变。;前端判 not_required:GET /tags 与 itinerary-document 前端零调用;tag-picker 唯一调用点 TagPickerModal catch 透 err.message 不吞(581008/581045 走通用 toast);价格字段/批量打回两文档补充零行为变化"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 协作域三只读端点补订单归属守卫 + 行程价格字段/团期批量打回契约说明
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8290
|
||||
> **Issue**: #8170
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台订单详情「标签」区、打标签弹窗、生成电子行程单(对客)、订单详情「行程安排」Tab、团期「查看需求」批量打回
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:`GET /v3/admin/order/{orderId}/tags`、`GET /v3/admin/order/{orderId}/tag-picker`、`GET /v3/admin/order/{orderId}/itinerary-document` 三个端点新增订单归属校验。
|
||||
- 前端以前以为的:只要拿到 token、打开订单详情页,就能对任意 `orderId` 调这三个接口拿到数据——改前这三个端点方法体零权限判断,任意后台角色都能读到他人订单的标签、打标签弹窗数据、完整电子行程单(含出行人、酒店、大交通)。
|
||||
- 实际新行为:非归属定制师、非 `ADMIN`/`SUPER_ADMIN`/`VEHICLE_MANAGER`、且不是在读团期子订单的团期管理员,会收到 `code=581008`;房务管理员/房务组长一律先被拒绝为 `code=581045`。请求体、响应体结构不变,只是多了这两种失败响应。
|
||||
- 同一次提交里顺带把 `GET /{id}/itinerary` 的四个价格字段(协议价快照、结算价快照、计划日单价、房型行协议价)与批量打回 `resourceType` 字段的既有契约写进了字段说明(**不是行为变化,是文档补充**):这四个价格字段是供应商采购价,不是对客报价;批量打回 `resourceType=ALL` 的车侧范围只覆盖游览车(TRAVEL),不含接送机(TRANSFER)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
#8170 AC-18 复核发现协作域三个只读端点(标签列表、打标签弹窗、电子行程单)方法体零归属校验——Controller 与 Service 都没有调用任何 `OrderViewGuard`,只要能拿到 JWT(任意后台角色)就能传任意 `orderId` 读到他人订单的标签与完整行程文档。本次在 `CollabService` 三个方法体内补 `OrderViewGuard.assertOrderReadable`。
|
||||
|
||||
同一份工单里还有两条**纯文档补充**(无代码行为变化):AC-2 把「团期管理员为什么能在行程 Tab 看到供应商成本价」这条 2026-09-22 已拍板的口径写进 `ItineraryVO` 字段注释;AC-11 把「批量打回 `ALL` 不含接送机」这条既有行为写进 `RejectRequirementReqVO` 与 `GroupBatchRequirementService#doReject` 的契约说明。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 查订单已挂标签 | GET | `/v3/admin/order/{orderId}/tags` | 新增归属校验 | 新增 `OrderViewGuard.assertOrderReadable`;非放行角色返 581008,房务/组长返 581045 |
|
||||
| 2 | 打标签弹窗数据 | GET | `/v3/admin/order/{orderId}/tag-picker` | 新增归属校验 | 同上;守卫排在 `OperatorHolder` 取当前操作人之前 |
|
||||
| 3 | 生成电子行程单(对客) | GET | `/v3/admin/order/{orderId}/itinerary-document` | 新增归属校验 | 同上;`documentType` 缺失时先于守卫返回业务码 400 |
|
||||
| 4 | 行程安排 Tab | GET | `/v3/admin/order/{id}/itinerary` | 字段说明变更 | 四个价格字段补注「供应商采购价,非对客报价」;接口行为、字段结构不变 |
|
||||
| 5 | 按户打回需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/reject` | 字段说明变更 | `resourceType` 契约补充:`ALL`/`VEHICLE` 车侧仅覆盖 TRAVEL;接口行为不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查订单已挂标签 `GET /v3/admin/order/{orderId}/tags`
|
||||
|
||||
**VO**: `无请求体 → OrderTagListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页展示已挂在该订单上的标签(扁平列表,不分类型)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| orderId | Path | Long | ✅ | 订单须存在(581430);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) | 订单 ID |
|
||||
|
||||
#### 出参 `Result<OrderTagListRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| attached | List<Object> | 当前订单已挂标签列表(扁平,不分类型) |
|
||||
| attached[].id | String | 标签主键(Long,超 JS 安全整数范围时序列化为字符串) |
|
||||
| attached[].orderId | String | 关联订单 ID |
|
||||
| attached[].tagName | String | 标签名 |
|
||||
| attached[].tagColor | String | 标签色值,如 #FF6B6B |
|
||||
| attached[].creator | String | 创建人姓名(定制师 or SYSTEM) |
|
||||
| attached[].createdAt | String | 创建时间,格式 yyyy-MM-dd HH:mm:ss |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2100542584106496001/tags
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"attached": [
|
||||
{
|
||||
"id": "2101000000000000001",
|
||||
"orderId": "2100542584106496001",
|
||||
"tagName": "VIP",
|
||||
"tagColor": "#FF6B6B",
|
||||
"creator": "张定制",
|
||||
"createdAt": "2026-09-20 10:15:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无标签时 `attached` 为空数组,不是 null:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "attached": [] }, "traceId": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次新增(TEST 实测,夹具订单 2100542584106496001,归属定制师为他人;CUSTOMIZER 非归属角色实测返回):
|
||||
|
||||
```json
|
||||
{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
房务管理员/房务组长(`ROOM_MANAGER`/`house_keeper_lead`)先于归属判定被拒绝:
|
||||
|
||||
```json
|
||||
{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
订单不存在(既有行为,未变):
|
||||
|
||||
```json
|
||||
{ "code": 581430, "message": "订单不存在", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 放行角色(TEST 实测 SUPER_ADMIN 正常返回):`ADMIN`/`SUPER_ADMIN` 恒放行;`VEHICLE_MANAGER` 恒放行;`GROUP_BATCH_MANAGER` 仅当目标订单挂在团期下(`order.groupBatchId != null`)才放行,散客单仍 581008;其余角色(定制师/客服/运营/财务等)必须是该订单的归属定制师(`adminId == order.consultantId`),否则 581008。
|
||||
- 房务管理员/房务组长无论订单归属如何,一律先于其他判定被拒绝为 581045(`OrderViewGuard.assertReadable`,`hl-order-service-v3/src/main/java/com/hulalv/order/core/guard/OrderViewGuard.java:279-281`)。
|
||||
- 判定顺序:先查订单是否存在(581430)→ 再判角色归属(581008/581045,`CollabService.java:172,174`);非请求上下文(MQ 回放/定时任务/内部 Feign/单测)无角色视为系统态放行。
|
||||
|
||||
---
|
||||
|
||||
### 2. 打标签弹窗数据 `GET /v3/admin/order/{orderId}/tag-picker`
|
||||
|
||||
**VO**: `无请求体 → List<TagPickerItemVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页点「打标签」按钮弹出的选择面板:标签库全集 + 当前订单已挂标记 + 游离标签(已挂但不在库里的)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| orderId | Path | Long | ✅ | 订单须存在(581401);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) | 订单 ID |
|
||||
|
||||
#### 出参 `Result<List<TagPickerItemVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| (根) | List<Object> | 弹窗选择项列表;先按标签库排序(isPinned DESC, sortOrder ASC, lastUsedAt DESC),游离标签追加末尾 |
|
||||
| [].tagName | String | 标签名 |
|
||||
| [].tagColor | String | HEX 色值,如 #5B8FF9 |
|
||||
| [].tagScope | String | SYSTEM=系统标签 / PERSONAL=个人标签;游离标签为 null |
|
||||
| [].isPinned | Boolean | 是否置顶(游离标签为 false) |
|
||||
| [].sortOrder | Integer | 排序值(游离标签为 null) |
|
||||
| [].selected | Boolean | 是否已挂在当前订单 |
|
||||
| [].inLibrary | Boolean | 是否来自标签库(false=游离标签,仅存在于 order_tag 中) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2100542584106496001/tag-picker
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{ "tagName": "VIP", "tagColor": "#5B8FF9", "tagScope": "SYSTEM", "isPinned": true, "sortOrder": 1, "selected": true, "inLibrary": true },
|
||||
{ "tagName": "临时标记", "tagColor": "#FF9900", "tagScope": null, "isPinned": false, "sortOrder": null, "selected": true, "inLibrary": false }
|
||||
],
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
标签库为空且订单无游离标签时返回空数组:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": [], "traceId": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次新增(TEST 实测,CUSTOMIZER 非归属角色实测返回):
|
||||
|
||||
```json
|
||||
{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
订单不存在(既有行为,未变):
|
||||
|
||||
```json
|
||||
{ "code": 581401, "message": "订单不存在", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 放行/拒绝口径与「查订单已挂标签」完全一致(同一个 `OrderViewGuard.assertOrderReadable`,见该接口业务边界第 1 条的角色表)。
|
||||
- 归属守卫排在 `OperatorHolder.get()` 取当前操作人之前(`CollabService.java:250` 早于 `:252`):`OperatorHolder` 判空只在「拿不到操作人」时拦截,拿得到操作人的越权请求它不管,不能替代归属校验。
|
||||
|
||||
---
|
||||
|
||||
### 3. 生成电子行程单(对客) `GET /v3/admin/order/{orderId}/itinerary-document`
|
||||
|
||||
**VO**: `无请求体 → OrderItineraryDocumentVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
生成对客视角的电子行程单/打印版/核价单(`CUSTOMER`/`CUSTOMER_PRINT`/`CUSTOMER_QUOTE` 三类 `documentType` 共用本接口,前端按值渲染不同模板)。签单(供应商视角 `sign-voucher`)、司机出团单(`print-itinerary`)走另外两个独立接口,不在本次变更范围内。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| orderId | Path | Long | ✅ | 订单须存在(581401);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) | 订单 ID |
|
||||
| documentType | Query | String | ✅ | `CUSTOMER` / `CUSTOMER_PRINT` / `CUSTOMER_QUOTE`;缺失返回业务码 400 | 文档类型(对客视角) |
|
||||
| includeResourceDetail | Query | Boolean | ❌ | 默认 true;`CUSTOMER` 场景可传 false 跳过 Feign | 是否调 Feign 拉资源详情,false 时节点 scenicDetail 等字段为 null |
|
||||
|
||||
#### 出参 `Result<OrderItineraryDocumentVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| documentType | String | 回显请求的文档类型 |
|
||||
| header | Object | 抬头信息(订单号等;`CUSTOMER_QUOTE` 才含 totalAmount) |
|
||||
| travelers | List<Object> | 出行人列表 |
|
||||
| days | List<Object> | 按天行程(含节点、场景/餐厅详情) |
|
||||
| hotels | List<Object> | 酒店段列表 |
|
||||
| feeNotes | List<Object> | 费用说明 |
|
||||
| supplies | List<Object> | 随行物资 |
|
||||
| transports | List<Object> | 大交通 |
|
||||
| summary | Object | 汇总信息 |
|
||||
| resourceDetailHealth | Object | 资源详情 Feign 健康度报告(降级不阻断) |
|
||||
| generatedAt | String | 生成时间,格式 yyyy-MM-dd HH:mm:ss |
|
||||
|
||||
(完整字段树见既有 Swagger `OrderItineraryDocumentVO`;本次变更不涉及该 VO 任何字段,此表只列顶层结构定位用)
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2100542584106496001/itinerary-document?documentType=CUSTOMER&includeResourceDetail=true
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"documentType": "CUSTOMER",
|
||||
"header": { "orderNo": "HL2026092012345" },
|
||||
"travelers": [],
|
||||
"days": [],
|
||||
"hotels": [],
|
||||
"feeNotes": [],
|
||||
"supplies": [],
|
||||
"transports": [],
|
||||
"summary": {},
|
||||
"resourceDetailHealth": { "feignOk": true, "feignError": null },
|
||||
"generatedAt": "2026-09-23 20:10:00"
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`includeResourceDetail=false` 或 Feign 调用失败时,各节点的资源详情字段(如 scenicDetail)为 null,`resourceDetailHealth.feignOk=false` 且 `feignError` 非空,接口仍返回 200,不阻断整份文档:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "resourceDetailHealth": { "feignOk": false, "feignError": "resource-service 调用超时" } }, "traceId": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次新增(TEST 实测,CUSTOMIZER 非归属角色实测返回):
|
||||
|
||||
```json
|
||||
{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
`documentType` 缺失(既有行为,未变;TEST 实测确认发生在归属守卫之前,源码为通用 Spring 参数绑定异常处理 `GlobalExceptionHandler#handleMissingParam`):
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "缺少必要参数: documentType", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
订单不存在(既有行为,未变):
|
||||
|
||||
```json
|
||||
{ "code": 581401, "message": "订单不存在", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 放行/拒绝口径与前两个接口完全一致。
|
||||
- `documentType` 缺失的 400 判定在**归属守卫之前**(Spring 参数绑定先于方法体执行),所以「非归属角色 + 不传 documentType」拿到的是 400,不是 581008;这不是本次改动,是既有行为,TEST 已实测确认。
|
||||
- 归属守卫(`CollabService.java:417`)与下游第 7 步 `OrderDetailService#getServiceStandard → requireOrderById` 里的同一道守卫会被连续调用两次,是有意保留、非冗余:本处这道是本端点自己的,下游那道挂在一个可选步骤上,不能替代(改动下游步骤的人不会打开本文件)。
|
||||
|
||||
---
|
||||
|
||||
### 4. 订单详情 - 行程安排 Tab `GET /v3/admin/order/{id}/itinerary`
|
||||
|
||||
**VO**: `无请求体 → ItineraryVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页「行程安排」Tab:配房/配车的需求与实配对照、未失活配车历史等。本次只补充四个价格字段的说明文字,接口行为、归属校验、响应结构均未变——该端点的归属守卫是既有能力,不是本次新增。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| id | Path | Long | ✅ | 订单须存在;当前登录角色须对该订单具备归属读权(既有能力,未变) | 订单 ID |
|
||||
|
||||
#### 出参 `Result<ItineraryVO>`(仅列本次文档变更涉及的字段,完整契约不在本次变更范围内)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| hotelGroup.assignments[].protoPrice | String | 协议价快照(元/间·晚,house protoPrice)。**本次补充说明:这是供应商采购价,不是对客报价** |
|
||||
| hotelGroup.assignments[].settlementPrice | String | 结算价快照(元/间·晚,house settlementPrice)。**本次补充说明:同样是供应商采购价,不是对客报价** |
|
||||
| vehicleGroup.assignments[].plannedDailyFee | String | 计划日单价(元/车·天,付给车队的采购价)。**本次补充说明:非对客报价** |
|
||||
| hotelGroup.requirement.days[].segments[].candidates[].rooms[].protocolPrice | String | 本房型行协议价(元/间·晚,可为 null)。**本次补充说明:这是供应商采购价,不是对客报价** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2100542584106496001/itinerary
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"hotelGroup": {
|
||||
"assignments": [
|
||||
{ "assignmentId": "700001", "hotelName": "云台山大酒店", "roomType": "大床房", "roomCount": 2, "protoPrice": "588.00", "settlementPrice": "688.00" }
|
||||
]
|
||||
},
|
||||
"vehicleGroup": {
|
||||
"assignments": [
|
||||
{ "plannedDailyFee": "800.00", "driverName": "王师傅" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
未配房/未配车时 `hotelGroup.assignments`/`vehicleGroup.assignments` 为空数组,各价格字段随之不出现:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "hotelGroup": { "assignments": [] }, "vehicleGroup": { "assignments": [] } }, "traceId": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
既有行为,未变:
|
||||
|
||||
```json
|
||||
{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 该端点走 `OrderController#getItinerary → OrderDetailService.assembleItinerary → requireOrderById`,仍是 `OrderViewGuard.assertOrderReadable`(`OrderDetailService.java:794-803`),**不是**本次新增,本次改动只补了字段说明。
|
||||
- 四个价格字段对**团期管理员**(`GROUP_BATCH_MANAGER`,仅限该订单挂在团期下)刻意不脱敏:这是 2026-09-22 管理者拍板的已知设计,理由是团期管理员核对「团期需求」与实配是否对得上,价格本身就是核对基准;同一条定案记在 `OrderViewGuard#assertOrderFinanceReadable` 的 javadoc(`OrderViewGuard.java:164-168`),本端点未被收窄到 `assertOrderFinanceReadable`。
|
||||
- 四个字段均为供应商采购成本,不是对客报价;本次只是把这条口径写进了字段注释,不是新发现的行为变化。
|
||||
|
||||
---
|
||||
|
||||
### 5. 按户打回需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/reject`
|
||||
|
||||
**VO**: `RejectRequirementReqVO → GroupBatchRequirementRejectRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期「查看需求」页批量打回:运营勾选若干户,把已提交需求退回定制师重填。本次只补充 `resourceType` 字段的契约说明,请求/响应结构、错误码均未变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| groupBatchId | Path | Long | ✅ | 团期须存在且处于可打回阶段(589501) | 团期主订单 ID |
|
||||
| orderIds | Body | List<Long> | ✅ | 1~200 户,服务端去重 | 被打回的子订单 ID 列表 |
|
||||
| reason | Body | String | ✅ | ≤500 字 | 打回原因 |
|
||||
| resourceType | Body | String | ❌ | `HOTEL` / `VEHICLE` / `ALL`,默认 `ALL`;**本次补充说明**:`VEHICLE`/`ALL` 车侧仅覆盖游览车(TRAVEL),不含接送机(TRANSFER) | 打回的资源类型 |
|
||||
|
||||
#### 出参 `Result<GroupBatchRequirementRejectRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期聚合主键 |
|
||||
| requirementConfirmed | Boolean | 团期需求整体确认标记(本端点成功后恒 false) |
|
||||
| rejected | List<Object> | 本次被打回的需求条目(每户每资源类型一条) |
|
||||
| rejected[].orderId | String | 子订单 ID |
|
||||
| rejected[].resourceType | String | 资源类型:HOTEL / VEHICLE |
|
||||
| rejected[].requirementId | String | 被打回的需求行 ID |
|
||||
| rejected[].sourceStatus | String | 打回前的需求状态:PENDING_REVIEW / PENDING |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"orderIds": [60123456789001, 60123456789002],
|
||||
"reason": "房间需求与套餐不匹配,请重新填写",
|
||||
"resourceType": "ALL"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "1867000000001",
|
||||
"requirementConfirmed": false,
|
||||
"rejected": [
|
||||
{ "orderId": "60123456789001", "resourceType": "HOTEL", "requirementId": "90011223344", "sourceStatus": "PENDING_REVIEW" }
|
||||
]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口没有空数据场景;所有勾选户都不可打回时整单拒绝(见错误响应 589534),不会返回 `rejected: []` 的成功响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次澄清的既有行为(`resourceType=ALL` 且勾选户只有接送机需求):
|
||||
|
||||
```json
|
||||
{ "code": 589534, "message": "子订单 {0} 不属于本团期、已退出或无可打回的需求", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
其余既有错误码未变:
|
||||
|
||||
```json
|
||||
{ "code": 589501, "message": "团期状态不允许当前操作", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 589535, "message": "子订单 {0} 已分房,请先由房务调整配房后再打回", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **本次澄清、代码逻辑未变**:`resourceType=ALL` 不等于「全部资源」——车侧只走游览车(TRAVEL),接送机(TRANSFER)需求不在本入口可见范围内。一户只有接送机需求时,不是「打回了但没生效」,是该户在候选筛选阶段就没被选中,因「无可打回的需求」落 589534。
|
||||
- 接送机需求要打回,走单户接口 `POST /v3/admin/order/{id}/vehicle-requirement/reject?kind=TRANSFER`,本次未变。
|
||||
- 任一户不可打回则整单拒绝、所有户零副作用(含合法户在内);不存在部分成功。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 请求 | 结果 |
|
||||
|------|------|------|
|
||||
| ✅ SUPER_ADMIN 读任意订单的标签/弹窗数据/行程单 | 上述三接口任一 | 200 |
|
||||
| ✅ 定制师读自己归属的订单 | 上述三接口任一,orderId=自己 consultantId 名下订单 | 200 |
|
||||
| ✅ 团期管理员读团期子订单(`order.groupBatchId != null`) | 上述三接口任一 | 200 |
|
||||
| ❌ 定制师/客服/运营/财务读他人归属的订单 | 上述三接口任一,orderId=非本人订单 | 581008(本次新增) |
|
||||
| ❌ 团期管理员读散客单(`order.groupBatchId == null`) | 上述三接口任一 | 581008(本次新增) |
|
||||
| ❌ 房务管理员/房务组长读任意订单的标签/弹窗数据/行程单 | 上述三接口任一 | 581045(本次新增,前置于归属判定) |
|
||||
| ❌ `itinerary-document` 不传 `documentType` | 任意角色 | 400「缺少必要参数: documentType」(既有行为,发生在归属守卫之前) |
|
||||
| ❌ 批量打回 `resourceType=ALL`、勾选户只有接送机需求 | `POST .../requirement/reject` | 589534(既有行为,本次补充契约说明) |
|
||||
|
||||
- 三个协作域端点(tags/tag-picker/itinerary-document)的归属判定口径完全一致,判定顺序恒为:订单存在性 → 房务前置拒绝(581045)→ ADMIN/SUPER_ADMIN/VEHICLE_MANAGER 放行 → 团期管理员限团期子订单放行 → 其余角色须为归属定制师,否则 581008(`OrderViewGuard.java:267-298`)。
|
||||
- `GET /{id}/itinerary` 的归属守卫本次未变(既有能力);仅字段说明变化。
|
||||
- 批量打回 `resourceType` 字段行为本次未变;仅补充契约说明,防止把 `ALL` 误读为「全部资源类型」。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无 Flyway migration、无 DDL、无表结构变更。
|
||||
- 三个协作域端点新增的归属守卫是纯内存态角色/字段判定,复用 Service 层判空后已持有的 `OrderInfo` 实体,零额外 DB 查询。
|
||||
- 批量打回 `doReject` 的写库范围与事务边界本次未变:仍在团级 `Lock4j` 锁 + `@Transactional(rollbackFor=Exception.class)` 内逐户跑 `rejectRequirementByGroupAdmin` + `groupBatchService.rejectRequirement`;任一户校验失败即整单回滚、零写入。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 非请求上下文(MQ 回放/定时任务/内部 Feign/单测)无角色 → 三个新增守卫的端点与既有的 `/itinerary` 端点均视为系统态直接放行(`OrderViewGuard.assertReadable` 捕获 `IllegalStateException` 后 `return`,`OrderViewGuard.java:271-276`)。
|
||||
- 房务角色的 581045 判定先于归属判定,与该订单是否属于本人无关——房务管理员/组长在这三个端点上无论如何都拿不到数据,只能通过配房相关接口工作。
|
||||
- 团期管理员的放行只看 `OrderInfo.getGroupBatchId() != null`(订单主表原始列),散客单一律 581008,即使该团期管理员对其他团期子订单有权限。
|
||||
- `itinerary-document` 的守卫在方法体第 2 步(订单判空之后)触发,比 `documentType` 缺失的 400 校验晚——所以「非归属角色 + 不传 documentType」拿到的是 400,不是 581008。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 接口 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `GET .../tags` | 无归属校验,请求体/响应体结构不变 | 新增 581008/581045 两种拒绝响应,请求体/响应体结构不变 |
|
||||
| `GET .../tag-picker` | 同上 | 同上 |
|
||||
| `GET .../itinerary-document` | 同上 | 同上 |
|
||||
| `GET .../itinerary` 四个价格字段 | `@ApiModelProperty` 文案未标注采购价/对客报价区分 | 文案补充「供应商采购价,非对客报价」;字段名、类型、序列化方式均不变 |
|
||||
| `resourceType`(打回请求体) | 字段说明未提及 TRAVEL/TRANSFER 边界 | 字段说明补充「ALL/VEHICLE 车侧仅覆盖 TRAVEL,不含 TRANSFER」;`@Pattern` 校验、默认值、字段名均不变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 任意后台角色传任意 orderId 访问已挂标签/打标签弹窗/电子行程单 | 200,直接返回目标订单数据(含他人订单) | 非放行角色 581008 或 581045,读不到他人订单数据 |
|
||||
| `resourceType=ALL` 批量打回、勾选户只有接送机需求 | 该户因「无可打回的需求」落 589534,行为未变 | 行为完全未变,仅补充契约说明 |
|
||||
| `/itinerary` 四个价格字段的语义 | 字段名暗示价格但未明确采购价/售价 | 字段注释明确标注为供应商采购价,防止误当对客报价展示 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:三个协作域端点对非放行角色(多数后台角色,若非订单归属定制师)是破坏性收紧——改前 200 能拿到数据,改后 581008/581045。对归属定制师、`ADMIN`/`SUPER_ADMIN`/`VEHICLE_MANAGER`,以及读团期子订单的团期管理员,行为不变仍 200。`/itinerary` 与批量打回两个纯文档改动零行为影响。
|
||||
- **前端是否必须同步上线**:三个协作域端点——如果前端当前允许任意角色打开任意订单的标签/打标签弹窗/生成行程单(例如客服临时查看非本人订单),上线后这部分角色会开始收到 581008/581045,前端需要能正确展示失败提示(走通用错误提示即可,不需要特殊 UI,但不能吞掉这个错误)。
|
||||
- **前端 workaround 清理点**:无——本次是后端补权限收紧,不涉及前端此前绕过某个限制的逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`GET /v3/admin/order/{orderId}/tags`、`GET /v3/admin/order/{orderId}/tag-picker`、`GET /v3/admin/order/{orderId}/itinerary-document` 三个端点新增归属校验;`GET /v3/admin/order/{id}/itinerary` 与 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/reject` 仅字段说明文案变化。
|
||||
- **零影响**:
|
||||
- `CollabAdminController` 的四个写端点(`addTag`/`deleteTag`/`patchTag`/`replaceTags`):本次未改动,仍走既有的 `OrderViewGuard.assertNotGroupBatchManagerWrite()`(只拒团期管理员写操作,不判定其余角色归属)。**已知缺口**:这四个写端点至今不判订单归属,读面收紧后形成「看不到但能改」,由 #8292 承接(口径未定)。
|
||||
- `OrderViewGuard.assertOrderFinanceReadable` 覆盖的财务/成本/流水端点:本次未改动其挂载。
|
||||
- 接送机相关的批量确认入口(`batchConfirmTransfer` 等)与单户打回接口 `POST .../vehicle-requirement/reject?kind=TRANSFER`:本次未变。
|
||||
- `ItineraryVO`、`RejectRequirementReqVO`、`GroupBatchRequirementRejectRespVO` 的字段名、类型、序列化方式、`@Pattern`/`@Size`/`@NotEmpty` 校验规则:全部未变。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-order-service-v3 dev-v3 @ `841ea7b06`,2026-09-23 20:07:37 部署,deploy-status STATE=ok,BEHIND 0(读数来源 #8170 评论 #60783)。
|
||||
|
||||
真实网关实测(夹具订单 `2100542584106496001`,归属定制师为他人):
|
||||
|
||||
```
|
||||
GET /v3/admin/order/2100542584106496001/tags CUSTOMIZER(非归属) → 581008 ✓
|
||||
GET /v3/admin/order/2100542584106496001/tag-picker CUSTOMIZER(非归属) → 581008 ✓
|
||||
GET /v3/admin/order/2100542584106496001/itinerary-document CUSTOMIZER(非归属) → 581008 ✓
|
||||
GET /v3/admin/order/2100542584106496001/tags SUPER_ADMIN(对照) → 200 ✓
|
||||
GET /v3/admin/order/2100542584106496001/tag-picker SUPER_ADMIN(对照) → 200 ✓
|
||||
GET /v3/admin/order/2100542584106496001/itinerary-document SUPER_ADMIN(对照) → 200 ✓
|
||||
GET /v3/admin/order/2100542584106496001/itinerary-document 不带 documentType → 400,先于守卫触发 ✓
|
||||
```
|
||||
|
||||
单元测试:本单随 PR 新增/改动测试文件 `CollabServiceTest`(+80 行,补三接口归属守卫放行/拒绝用例)、`CollabServiceItineraryDocumentTest`(+60 行,补 itinerary-document 归属守卫用例)、`GroupBatchRequirementServiceTest`(+50 行,补 resourceType 契约用例),随 PR 一并合并。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8170](https://git.1814.love/wx/HL/issues/8170)
|
||||
- 关联 PR: [wx/HL#8290](https://git.1814.love/wx/HL/pulls/8290)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8170](https://git.1814.love/wx/HL/issues/8170)
|
||||
- **PR**: [#8290](https://git.1814.love/wx/HL/pulls/8290)
|
||||
- **Merge commit**: [841ea7b06](https://git.1814.love/wx/HL/commit/841ea7b06)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,348 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8194"
|
||||
title: "配置位守卫 582117 的触发集合扩大到「除 DRIVER / OTHER 以外的全部角色」"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "工单 #8194 补的是 #8148 留下的残留缺口。角色↔人员类型的守卫 582117 判的是「该角色在配置位字典里查不到归属位」,而它的分流判据(GroupBatchStaffSlotResolver#participatesInSlotsByFallback)读的是编译期常量兜底名单,只认 GUIDE / LEADER / PHOTOGRAPHER。问题是 #7079 / #8148 立下的能力恰恰是「业务在字典页面加一行就能把某个人员类型配进配置位、不发版」——靠字典新增出来的角色永远不在这张常量名单里,于是「字典给某位加一行 → 有人按它配了人 → 那行又被删掉」这一形态仍走静默放行分支:接口 200、脏行落库、并异步扇出到团内全部活跃订单,该角色的 582114 当场失效,要到结算对账才发现人岗对不上。关键一点是撞上它不需要有人手搓请求——resolveStoredStaffRole 会把落库的 staff_role 改写成人员的真实类型(字典角色),前端回显后原样提交回来,系统自己就会产出那个落进缺口的 staffRole 值。修法(2026-09-23 定案 B,判据翻面):把「本该有位」的判据从「在不在兜底名单里」换成「不在结构性豁免闭集 STRUCTURALLY_SLOT_EXEMPT_ROLES(= DRIVER / OTHER)里」,豁免闭集落成显式常量集合并从 SettlementStaffRoleEnum 取值,方法名同步改为 requiresSlotMembership(改完它不再读 fallbackTypes(),名字与语义必须同批改)。行为变化:GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER 三个「只可能靠字典纳入配置位」的角色,在「字典里查不到归属位」时由静默 200 落库变为返回 582117;DRIVER / OTHER 照旧放行(OTHER 的常态就是不属于任何配置位,保护它会拒掉每一次正常的杂项人员配置,那是删功能不是补洞)。已知边界(按 #8194 决策点 4 有意保留、不在本单修):OTHER 在 VALID_STAFF_TYPES 里,业务能把 OTHER 写进某个位的字典再删掉,那一形态仍走静默放行。生产代码由 PR #8259 合入 dev-v3(squash 提交 f6d21648b)。2026-09-23 测试服已实测:PUT /v3/admin/group-batch/2102640646949105666/staff 提交 staffRole=GUIDE_ASSISTANT 返回 code=582117(HTTP 200,文案完整),读回确认零写入;同端点 staffRole=DRIVER 与 staffRole=OTHER 均返回 code=200 成功;探测插入的两行已用 scopeRoles=[DRIVER,OTHER] + staffList=[] 还原,读回与探测前逐字段一致。契约(路径 / 方法 / 权限 / 入参字段与取值域 / 成功响应结构)零变更——BatchStaffConfigReqVO 只加了 javadoc(含该表)。前端结论(2026-09-23):5821xx 全族在 hl-ui v2.1 无专门分支,统一由拦截器按 code 非成功 toast 后端 message;582117 文案已点名角色并指向配置位字典页,完整直出即可。命中后正确动作是去字典页补行,重试仍被拒不会误操作——与 #8123/#8148 既定拍板一致,维持通用 toast,不做专门引导,零改动闭环。后端订正三处(降级态字典角色同拒 582117/示例 payload 纠错/补缓存窗口与存量脏行边界)前端已审阅:均为后端行为澄清,契约零变化,文案仍拦截器直出,前端 not_required 结论不变。"
|
||||
updated_at: "2026-09-23"
|
||||
# ⚠️ 本条目于 2026-09-23 由独立对抗评审复核后订正过三处:①降级态(字典服务不可用)对字典角色同样返回 582117,原文「不会变成一律拒绝」只对内置角色成立;②请求示例原用了修复后必被拒的 payload;③补记缓存窗口与存量脏行两条边界。
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置: 配置位缺失守卫 582117 的触发集合扩大到全部非豁免角色(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8259(squash 提交 `f6d21648b`)
|
||||
> **Issue**: #8194(前置 #8148 / #8122 / #7079)
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情页的「配导游 / 配摄影」弹窗保存动作
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**这个端点返回 `582117` 的角色集合变大了**,路径、入参、成功响应结构一律不变。
|
||||
|
||||
以前只有 `GUIDE` / `LEADER` / `PHOTOGRAPHER` 三个内置角色在「字典里查不到归属位」时会被 582117 拒掉;现在**除 `DRIVER` 与 `OTHER` 以外的任何角色**都会被拒,其中就包括业务自己通过配置位字典启用过的 `GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`。
|
||||
|
||||
调用方以前以为的是:`staffRole=GUIDE_ASSISTANT` 这类「字典角色」提交时不会触发 582117(它压根不在守卫的名单里)。
|
||||
实际现在是:**它同样会触发 582117**,而且这批请求本来就是脏数据(角色没有任何人员类型校验背书)。
|
||||
|
||||
`DRIVER` / `OTHER` 的行为**不变**,仍放行。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
配置位成员集合自 #7079 起由数据字典决定(`group_batch_staff_slot_guide` / `group_batch_staff_slot_photographer`),业务在字典管理页面加一行就能把某个人员类型配进这个位,**不发版**。`GroupBatchStaffSlotResolver` 的类 javadoc 原文就是这句:「业务在字典管理页面给 `group_batch_staff_slot_guide` 加一行 `STUDY_TEACHER`,研学老师就能配进导游位,不发版」。
|
||||
|
||||
#8148 把「角色 → 允许的人员类型」这半边也迁到了字典,并引入 582117:当某角色在字典里查不到归属位时,本该有位的角色要被拒,而不是沿袭改前的「一律不校验」。但 #8148 的分流判据读的是**编译期常量兜底名单**(`GUIDE` / `LEADER` / `PHOTOGRAPHER`),它恰恰**不包含**任何「靠字典新增出来的角色」——那正是这个新能力的产物。结果是:
|
||||
|
||||
| 「查不到归属位」的成因 | #8148 之后的行为 | 是否正确 |
|
||||
|---|---|---|
|
||||
| `DRIVER` / `OTHER` 这类结构性不参与配置位的角色 | 放行 | ✅ 正确 |
|
||||
| 内置角色(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)被从仍非空的字典里删掉 | 拒绝 582117 | ✅ 正确 |
|
||||
| **字典启用过的角色**(`GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`)被删回 | **静默放行** | ❌ 缺口 |
|
||||
|
||||
第三种形态的失败后果与加 582117 之前**逐字相同**:HTTP 200、`Result.success`、前端看不到任何异常;落库成功并异步扇出到团内全部活跃订单;静默放行分支**一行日志都不打**。
|
||||
|
||||
而撞上它**不需要有人手搓请求**:
|
||||
|
||||
1. 业务给导游位字典加一行 `GUIDE_ASSISTANT`;
|
||||
2. 前端按导游位提交 `staffRole=GUIDE`、该人真实 `staffType=GUIDE_ASSISTANT` → 校验通过 → `resolveStoredStaffRole` 把**落库的 `staff_role` 改写成 `GUIDE_ASSISTANT`**;
|
||||
3. 下次打开弹窗,回显的既有行角色就是 `GUIDE_ASSISTANT`,提交回来 `staffRole=GUIDE_ASSISTANT`;
|
||||
4. 此后运维把那行字典删掉 ⇒ 第 3 步那种请求走进静默分支,**且此时 `staffRole=GUIDE_ASSISTANT` 配任何 `staffType` 都能过**。
|
||||
|
||||
**修法(#8194 决策点 5 定案 B:判据翻面)**:把「本该有位」的判据从「在不在兜底名单里」换成「**不在结构性豁免闭集里**」。豁免闭集 = `{DRIVER, OTHER}`(#8194 决策点 4 定案),落成 `GroupBatchStaffSlotResolver#STRUCTURALLY_SLOT_EXEMPT_ROLES` 这一个显式常量集合。方法随之改名 `participatesInSlotsByFallback` → `requiresSlotMembership`——改完它不再读 `fallbackTypes()`,名字与语义必须同批改,否则留下「命名断言别处行为」的静默漂移点。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改接口 | 582117 的触发角色集合扩大;路径 / 入参 / 权限 / 成功响应结构不变 |
|
||||
|
||||
网关无改动(没有新增 `/admin/` Controller,路由不动)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期人员配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「配导游 / 配摄影」弹窗点保存。按 `scopeRoles`(不传即整期)软删旧配置、写入新配置、异步扇出到各活跃子订单,并按配置结果回填 `guide_ready` / `photographer_ready`。
|
||||
|
||||
🔴 **路径参数是产品侧排期 ID,不是团期聚合主键**。端点名叫 `group-batch`,但 `{productBatchId}` 吃的是 `group_tour_batch.batch_id`。响应体里另有一个 `groupBatchId`,那才是订单侧 `order_group_batch` 的主键,两者不是同一个值。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 正整数,产品侧排期 ID | 团期所属班期;未建团返 589553 |
|
||||
| scopeRoles | body | List<String> | 否 | 传了就不能是空数组;元素非空白且须在员工角色取值域内 | 限定本次覆盖的角色范围,不传则整期覆盖 |
|
||||
| staffList | body | List<Item> | 是(可在服务层为空数组) | `@NotNull`;`[]` 表示清空覆盖范围内的配置 | 覆盖范围内的最终状态 |
|
||||
| staffList[].staffId | body | Long | 是 | 须命中候选人员 | 人员 ID |
|
||||
| staffList[].staffRole | body | String | 是 | `@NotBlank` + `@Pattern`,须与人员类型相符,否则 582114 | 角色取值域见「六.5」 |
|
||||
| staffList[].sortOrder | body | Integer | 否 | — | 展示排序,默认 0 |
|
||||
| staffList[].remark | body | String | 否 | `@Size(max=500)` | 备注 |
|
||||
|
||||
#### 出参 `Result<BatchStaffConfigRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| productBatchId | Long | 回显路径参数 |
|
||||
| groupBatchId | Long | 团期聚合主键,与路径参数不是同一个值 |
|
||||
| staffList | List | 保存后的整期最终状态(含 `staffRoleName` / `reporterRankName` 等中文 Label) |
|
||||
| affectedOrderCount | Integer | 本次扇出触及的活跃子订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/group-batch/2102640646949105666/staff HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"staffList":[{"staffId":1002,"staffRole":"GUIDE","sortOrder":0,"remark":"hl8194-ok"}]}
|
||||
```
|
||||
|
||||
> ⚠️ 把上面的 `staffRole` 换成 `GUIDE_ASSISTANT`(或其他字典角色)在**修复后**会返回 582117,不再是 200——见「错误响应」。这正是本单的行为变化。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
成功时:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"productBatchId": "2102640646949105666",
|
||||
"groupBatchId": "2102640736900116481",
|
||||
"affectedOrderCount": 1,
|
||||
"staffList": [
|
||||
{"id": "2102640914386284545", "staffId": 1002, "staffRole": "GUIDE", "staffRoleName": "导游", "staffName": "李雪梅", "reporterRank": "NONE"}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`staffList` 传空数组即清空覆盖范围内的配置,返回 200,`staffList` 为空数组、`affectedOrderCount` 为实际扇出订单数。
|
||||
|
||||
配置位字典读挂 / 读空时,`typesOf` 回落内置默认值(导游位 `GUIDE+LEADER`、摄影位 `PHOTOGRAPHER`):**内置角色**(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)的保存照常成功,这条降级保证本单未动。
|
||||
|
||||
⚠️ **但内置名单之外的角色不是这样**:`GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER` 不在兜底名单里,字典服务不可用期间它们依然「查不到归属位」⇒ **同样返回 582117**。也就是说 582117 现在有两种成因:①字典可读、但该角色不在任何配置位里(修复动作是补回那一行字典);②字典服务不可用、且该角色不是内置兜底成员(修复动作是恢复字典服务)。**文案当前不区分两者**,排查时请先确认字典服务可用性。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
新增会命中的情境:**字典里查不到该角色的归属位,且该角色不在豁免闭集里**(`{0}` 由运行期填入角色名):
|
||||
|
||||
```json
|
||||
{"code": 582117, "message": "角色 GUIDE_ASSISTANT 未归属任何人员配置位,无法校验人员类型;请检查配置位字典是否被误删", "data": null, "success": false}
|
||||
```
|
||||
|
||||
本端点此前已有、本次不改动的其余业务码(一并列出便于对照):
|
||||
|
||||
```json
|
||||
{"code": 582114, "message": "所选人员的角色与其人员类型不符,请重新选择", "data": null, "success": false}
|
||||
{"code": 582115, "message": "提交的人员角色超出本次保存声明的范围", "data": null, "success": false}
|
||||
{"code": 582116, "message": "本次保存声明的角色范围(GUIDE)只覆盖了配置位的一部分,还缺少 LEADER;同一配置位的角色必须一起声明,否则位内其余人员会被留在库里", "data": null, "success": false}
|
||||
{"code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false}
|
||||
```
|
||||
|
||||
全部走 **HTTP 200**,不是 5xx。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **582117 有两种成因,排查顺序是先服务后内容**:①字典可读、但该角色不在任何配置位里(被删掉,或从来没配过);②**字典服务(hl-user-service)不可用**而该角色又不是内置兜底成员(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)。此时文案说的「字典是否被误删」会指错方向——先去字典页面看会发现那行可能压根没删过。
|
||||
- **被判定的不是「你这个角色对不对」,而是「这个角色的归属位是不是丢了」**。角色本身合法(在 `@Pattern` 取值域内)但字典里没有它的位 → 582117;位找得到但人岗不符 → 582114。两者互补。
|
||||
- **修复动作在配置位字典页面,不在这个弹窗里**,所以文案点名角色并指向字典,适合原样透出给运营。
|
||||
- **豁免闭集是 `DRIVER` 与 `OTHER`**,两者在任何字典状态下都不做角色↔人员类型校验,582117 与 582114 都不触发。
|
||||
- 被 582114 / 582115 / 582116 / 582117 任一码拒绝时**零写入**——所有校验闸都在写库之前,且发生在注册 `afterCommit` 扇出回调之前,回读会看到与提交前逐字相同的配置。
|
||||
- 导游位并收 `GUIDE` 与 `LEADER`:只传 `["GUIDE"]` 却选了领队会命中 582115,只传 `["GUIDE"]` 而位内还有 LEADER 会命中 582116。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### 契约面零变化,行为面有变化
|
||||
|
||||
| 面 | 是否变化 | 依据 |
|
||||
|---|---|---|
|
||||
| 路径 / HTTP 方法 / 权限码 | 不变 | Controller 未动 |
|
||||
| 入参字段、类型、必填性、`@Pattern` 取值域 | 不变 | `BatchStaffConfigReqVO` 只加了 javadoc |
|
||||
| 成功响应结构 | 不变 | `BatchStaffConfigRespVO` 未动 |
|
||||
| **582117 的触发角色集合** | **变化** | Service 的分流判据翻面 |
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 内置角色、字典正常 | `{"staffList":[{"staffId":1002,"staffRole":"GUIDE"}]}` | 200,落库 `staffRole=GUIDE` |
|
||||
| ✅ 结构性豁免角色 | `{"scopeRoles":["DRIVER"],"staffList":[{"staffId":1002,"staffRole":"DRIVER"}]}` | 200,落库 `staffRole=DRIVER` |
|
||||
| ✅ 结构性豁免角色 | `{"scopeRoles":["OTHER"],"staffList":[{"staffId":1002,"staffRole":"OTHER"}]}` | 200,落库 `staffRole=OTHER` |
|
||||
| ✅ 字典已收录该角色 | 字典里 `group_batch_staff_slot_guide` 含 `GUIDE_ASSISTANT`,提交 `staffRole=GUIDE` 且该人 `staffType=GUIDE_ASSISTANT` | 200,落库 `staffRole=GUIDE_ASSISTANT` |
|
||||
| ❌ 字典角色但字典里已查不到位 | `{"staffList":[{"staffId":1002,"staffRole":"GUIDE_ASSISTANT"}]}` | 200 + `code=582117`,零写入 |
|
||||
| ❌ 提交越界的人 | `{"scopeRoles":["GUIDE"],"staffList":[{"staffId":1002,"staffRole":"LEADER"}]}` | 200 + `code=582115`,零写入 |
|
||||
|
||||
### 调用方的正确动作
|
||||
|
||||
- **命中 582117 时不要重试**,也不要自动改写 `staffRole`——正确动作是**去配置位字典页面把缺的那一行补回来**(或确认为什么它被删了),再重试保存。文案里的角色名就是缺的那一个。
|
||||
- **判成败一律看 `code`,不要看 HTTP status**:本端点的业务失败全是 HTTP 200。
|
||||
- 保存成功后若调用方缓存了 `guide_ready` / `photographer_ready`,需重新拉取团期详情。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 保存:按 `scopeRoles` 范围(不传即整期)软删旧配置行 → 写入新配置行 → 注册 `afterCommit` → 异步扇出到各活跃子订单(`order_staff_assignment` 中 `source=GROUP_BATCH` 的行整删整插)→ 回填团期行的两个 ready 标志。
|
||||
- 本次改动**不改变任何一步的写内容**,只改变「哪些请求会被挡在写库之前」。
|
||||
- 被 582114 / 582115 / 582116 / 582117 任一码拒绝时**零写入**:校验发生在 `buildToInsert` 与 `doSaveInTransaction` 之前,且 `afterCommit` 扇出回调**尚未注册**(注册动作在事务段内部、软删与插入之后)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **`DRIVER` / `OTHER` 不受本单影响**:两者在任何字典状态下都放行,与改前一致。
|
||||
- **字典读挂 / 读空不降级成拒绝**:`typesOf` 回落内置默认值,内置角色(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)照常保存。
|
||||
- **将来往 `@Pattern` 里加第 9 个取值**而忘了判断它属不属于豁免闭集时,新角色默认落进「本该有位」侧 ⇒ 那一侧是**拒绝**(响亮失败、当场被发现),不是静默放行。失败方向正确。
|
||||
- **已知边界,不在本单修**:`OTHER` 在 `VALID_STAFF_TYPES` 里,业务**能**把 `OTHER` 写进某个位的字典再删掉,那一形态仍走静默放行。判据是:`OTHER` 的常态化就是不属于任何配置位,把它移出豁免闭集会拒掉每一次正常的杂项人员配置;而「把『其他』纳入『导游位』」本身是配置位语义的误用(等于宣告「其他类型的人算导游」),错的是那行字典。
|
||||
- **存量脏行的连带影响(本单不修,但必须知道)**:字典行被删之后库里已有的 `staff_role='GUIDE_ASSISTANT'` 行不再属于任何配置位,整位守卫 582116 也判不到它,这些行不会被 `scopeRoles` 的删除窗口覆盖或重建。⚠️ 更要紧的是:**这类存量行会被前端原样回显提交**,而校验是**逐行**做的——只要该团期的保存请求里带上这样一行且 `staffRole` 仍在缺口集合内,**整次保存(含不传 `scopeRoles` 的整期覆盖)都会返回 582117**。是否已有这类存量行未在本次取证中统计(生产库只读盘点属独立动作)。治理另立工单。
|
||||
- **缓存窗口**:`typesOf` 是固定 5 分钟 TTL 的实例本地缓存(`@VettedLocalCache`)。字典改完到全部实例生效之间,同一请求可能一台放行、一台返回 582117。所以「删回后不再静默放行」是**有界**结论:最长 5 分钟、按实例收敛。
|
||||
- **判据不认识「历史」**:`requiresSlotMembership` 只看字典**当前**内容。因此「从没在任何配置位字典里出现过」与「加过又被删回」走同一条拒绝分支,而文案一律写成「是否被误删」。首次配置某类人员(业务还没往字典加那一行)时会看到这句——正确动作是**去对应配置位字典加一行**,不是去查删除记录。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### staffRole(取值域来自 `BatchStaffConfigReqVO.STAFF_ROLE_PATTERN`)
|
||||
|
||||
**所属字段**: `staffList[].staffRole` / `scopeRoles[]` | **类型**: `String`
|
||||
|
||||
下表说的是「**该角色在配置位字典里查不到归属位时**」修复后走哪条分支。**分母就是从 `@Pattern` 常量抄下来的这 8 个**(并有单测 `GroupBatchStaffRoleSlotGuardTest#patternValueDomain_classifiedOneByOne` 机械守住:常量改了而表没跟,用例先红)。
|
||||
|
||||
| 取值 | 分支 | 理由 |
|
||||
|---|---|---|
|
||||
| `GUIDE` | 拒绝 582117 | 导游位内置成员;默认字典下查得到位,查不到说明那一行被删了 |
|
||||
| `LEADER` | 拒绝 582117 | 同上(导游位并收导游与领队) |
|
||||
| `PHOTOGRAPHER` | 拒绝 582117 | 摄影位内置成员;查不到说明那一行被删了 |
|
||||
| `GUIDE_ASSISTANT` | **拒绝 582117(本单新增覆盖)** | 只可能靠字典被纳入配置位;「加过再删回」正是本单修的形态 |
|
||||
| `STUDY_TEACHER` | **拒绝 582117(本单新增覆盖)** | 同上 |
|
||||
| `LIFE_TEACHER` | **拒绝 582117(本单新增覆盖)** | 同上 |
|
||||
| `DRIVER` | 放行 | 结构性豁免:车务派车投影产生,资源域没有这个人员类型,也配不进任何位的字典 |
|
||||
| `OTHER` | 放行 + 已知边界 | 结构性豁免:常态就是不属于任何配置位,保护它会拒掉正常的杂项人员配置。代价是「`OTHER` 被某位字典纳入后再删掉」仍静默放行,本单不修 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `staffRole=GUIDE_ASSISTANT`(字典里查不到归属位) | **静默 200 + 落库 + 异步扇出**,582114 当场失效 | 582117,零写入 |
|
||||
| `staffRole=STUDY_TEACHER` / `LIFE_TEACHER`(同上) | 同上 | 582117,零写入 |
|
||||
| `staffRole=LEADER` 被从非空字典删掉 | 582117(#8148 已建立) | **不变** |
|
||||
| `staffRole=DRIVER` / `OTHER` | 放行 | **不变** |
|
||||
| 字典读挂 / 读空(内置角色) | 回落内置默认值,放行 | **不变** |
|
||||
| 字典读挂 / 读空(字典角色 `GUIDE_ASSISTANT` 等) | **静默 200** | **582117,零写入**(降级态无法区分「被删掉」与「读不到」,见「六、边界行为」) |
|
||||
| 判据实现 | `participatesInSlotsByFallback` 读编译期常量 `fallbackTypes()` | `requiresSlotMembership` 判「不在结构性豁免闭集 `{DRIVER, OTHER}` 里」 |
|
||||
| 路径 / 入参 / 权限 / 成功响应结构 | — | **零变化** |
|
||||
|
||||
### 字段级对比
|
||||
|
||||
无字段变化(`BatchStaffConfigReqVO` 只增加 javadoc,`BatchStaffConfigRespVO` 未动)。
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否(契约零变化)。但存在**面向管理后台的行为变化**:原本能保存成功的几个角色的请求会开始被 582117 拒——那批请求本就是脏数据(角色没有任何人员类型校验背书)。
|
||||
- **前端是否必须同步上线**: **否**。5821xx 这一族在 hl-ui v2.1 上没有专门分支,统一由 `src/utils/request.js` 的响应拦截器按「code 非成功值」toast 后端 message,故 582117 在这几个角色上出现时前端零改动即有基本行为(文案本身已是可直接展示的完整句子,并指向修复动作所在处)。
|
||||
- **前端可选优化**:命中 582117 时提示语可引导运营去「配置位字典」页面,并保留用户已填表单;不引导也不会误操作(重试仍会被拒)。
|
||||
- **前端 workaround 清理点**: 无。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **零影响**:
|
||||
- 路径、HTTP 方法、权限码、网关配置
|
||||
- 入参字段、类型、必填性、`@Pattern` 取值域
|
||||
- 成功响应结构与字段
|
||||
- 候选人查询(`/staff/candidates`、`/staff/candidates/page`)与配置列表查询(`GET /staff`)
|
||||
- 报账人等级设置口 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`
|
||||
- 子订单自己单独派的人员(`order_staff_assignment` 中 `source` 非 `GROUP_BATCH` 的行)
|
||||
- `582114` / `582115` / `582116` / `100503` 四个既有码的触发条件与文案
|
||||
- `assertScopeCoversWholeSlots`(582116)与 `scopeRoles` 的任何判定逻辑
|
||||
- `DRIVER` / `OTHER` 的放行口径
|
||||
- 数据库表结构与 Flyway(本单无表变更、无迁移脚本)
|
||||
- 小程序端(团期人员配置无小程序写口)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
2026-09-23,`api.test.1814.love:9443`(网关),分支 `dev-v3`,order-v3 活体 `f6d21648b`(双实例 8086 / 8186,滚动部署后均 UP)。
|
||||
|
||||
**取证批次**: `productBatchId=2102640646949105666`(#8123 / #8148 同一条取证批次),人员 `staffId=1002`(资源域真实 `staffType=GUIDE`)。
|
||||
|
||||
| 项 | 请求 | 结果 |
|
||||
|---|---|---|
|
||||
| 探测前快照 | `GET /v3/admin/group-batch/2102640646949105666/staff` | 200,1 行(`staffId=1002, staffRole=GUIDE`) |
|
||||
| **本单核心(修复后)** | `PUT` 同端点,`{"staffList":[{"staffId":1002,"staffRole":"GUIDE_ASSISTANT"}]}` | **HTTP 200,`code=582117`**,message「角色 GUIDE_ASSISTANT 未归属任何人员配置位,无法校验人员类型;请检查配置位字典是否被误删」 |
|
||||
| **零写入** | 上一步之后 `GET` 同端点读回 | 与探测前快照**逐字段一致**(拒绝路径未注册 afterCommit 扇出回调,未进事务段) |
|
||||
| 阴性对照(`DRIVER`) | `PUT` `{"scopeRoles":["DRIVER"],"staffList":[{"staffId":1002,"staffRole":"DRIVER"}]}` | HTTP 200,`code=200` 成功 |
|
||||
| 阴性对照(`OTHER`) | `PUT` `{"scopeRoles":["OTHER"],"staffList":[{"staffId":1002,"staffRole":"OTHER"}]}` | HTTP 200,`code=200` 成功 |
|
||||
| 现场还原 | `PUT` `{"scopeRoles":["DRIVER","OTHER"],"staffList":[]}` | 200;读回与探测前快照**逐字段一致** |
|
||||
|
||||
**测试环境上无法构造、因此未取活体样本的两项**(原因是环境性的、与实现无关):
|
||||
|
||||
- **AC-3「字典读挂回落内置默认值」**:需要让测试服的 `DictFeignClient` 整体失败,属环境级故障注入,会波及同环境其他使用者。该行为由真库用例 `GroupBatchStaffSaveConfigSlotCompletenessMysqlTest#saveConfig_dictFeignFails_fallbackKeepsSlotRolesAccepted` 与单元用例 `GroupBatchStaffRoleSlotGuardTest#dictUnavailable_fallbackKeepsBuiltInRolesAccepted` 覆盖(真实解析器 + 打桩 Feign,回落链路真跑)。
|
||||
- **AC-4 的测试服活体样本**(把内置角色从**仍非空**的配置位字典里删掉):字典是全站共享数据,删行的副作用会落到所有人的角色下拉框上,未构造。该行为由真库用例 `#saveConfig_dictDropsLeaderFromGuideSlot_mismatchedStaffTypeRejectedNotSilentlySkipped` 覆盖。
|
||||
|
||||
**证据文件**(脱敏、非空,已登记进 task capsule 的 evidence manifest):`D:/work2/.tmp/issue8194/evidence/`(`gateway-verification.json` / `gateway-8194.log` / `gw-1-before.json` … `gw-5-after-cleanup.json` / `gw-summary.json`)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8194(本单)、#8148(582117 的引入与残留缺口)、#8122(582116 整位覆盖守卫)、#7079(配置位字典化)
|
||||
- PR #8259(squash 提交 `f6d21648b`)
|
||||
- 同端点上一版条目:`changelogs-v2/2026-09/23_8123_团期staff保存新增并发抢锁与配置位字典守卫错误码-修改接口-管理后台.md`
|
||||
- 错误码定义:`hl-order-service-v3/src/main/java/com/hulalv/order/assignment/errorcode/AssignmentErrorCode.java`(582117)
|
||||
- 判据与豁免闭集:`hl-order-service-v3/src/main/java/com/hulalv/order/assignment/service/GroupBatchStaffSlotResolver.java`(`STRUCTURALLY_SLOT_EXEMPT_ROLES` / `requiresSlotMembership`)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8194](https://git.1814.love/wx/HL/issues/8194)
|
||||
- **PR**: [#8259](https://git.1814.love/wx/HL/pulls/8259)
|
||||
- **Merge commit**: `f6d21648bada5d0d465cd454390ba6bad923869a`
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端**: mmg(582117 提示语可引导至配置位字典页面,非必须)
|
||||
@@ -0,0 +1,385 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8202"
|
||||
title: "子订单行程用车车型大类接入车队字典校验(新增 582032/582033)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c314c8c67ddc61b8a1e4f4373d79e5a731b13272"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "PR #8224 已合并 dev-v3(b439565be)并滚动部署 TEST 双实例,Nacos 注册均 healthy:true。用车需求提交/调整两条写路径共用的车型大类校验链新增车队活字典比对,新增 582032/582033 两码;存量码 582022(填法不合法)不变。真实网关实测:两端点各以 vehicleType=\"__NOT_A_REAL_CATEGORY__\" 提交均返回 582022(填法层拦截优先于字典层)。前端 v2.1 已提前于 7df292624(09-23 09:27)改为车型字典下拉,新提交不会撞新码;仅存量已落库的非字典值在编辑态回显仍需前端显式提示。 | 2026-09-23 mmg 收尾交付:存量回显占位提示+fail-closed 拦截+JSDoc 订正,随 #8221 同 commit"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 用车需求模块: 子订单行程用车车型大类接入车队字典校验
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8224
|
||||
> **Issue**: #8202
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台子订单用车需求提交/修改、订单调整统一提交中的用车/接送机车型校验
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:`vehicleType` 归一成功(如 `大巴`→`bus`)后,新增一次**车队活字典**比对;该大类若已整类下线(车队活字典无一行在架代表),提交被拒。
|
||||
- 前端以前以为的:只要 `vehicleType` 能归一为 `suv/mpv/bus/sedan` 之一(或直接传规范 key),提交就会成功。
|
||||
- 实际新行为:归一成功只是必要条件,还须该大类**当前在车队活字典内**;否则报新码 `582032`。**存量码 `582022`(这串字符连大类都归一不出来)保持不变、且仍是校验链第一关**——按当前测试库数据,前端实际会撞上的是 `582022`(自由文本/拼写问题),`582032` 只在大类被整类下线时触发(详见下方六.5/六.6)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 提交/修改/调整用车需求 | PUT | `/v3/admin/order/{id}/vehicle-requirement` | 校验链新增字典比对 | `fleet[].vehicleType` 归一后追加车队活字典校验 |
|
||||
| 2 | 调整订单统一提交 | POST | `/v3/admin/order/{id}/adjustment/submit` | 校验链新增字典比对 | `updates.vehicleRequirement.fleet[]`/`updates.transferRequirement.fleet[]` 复用同一条校验链 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 提交/修改/调整用车需求 `PUT /v3/admin/order/{id}/vehicle-requirement`
|
||||
|
||||
**VO**: `VehicleRequirementReqVO → VehicleRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端子订单详情页提交/编辑用车需求(TRAVEL 行程用车 / TRANSFER 接送机)时调用;三分支由后端自动判断(无 active 需求=INIT_SUBMIT,PENDING=PENDING_EDIT,DONE=DONE_ADJUST),前端不用区分分支、始终整份提交。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| id | Path | Long | ✅ | 订单 ID | 目标订单 |
|
||||
| kind | Body | String | ❌ | 正则 `TRAVEL\|TRANSFER` | 需求类别,缺省按订单类型推断 |
|
||||
| fleet | Body | Array\<FleetItem\> | ✅ | 非空 | 车型组合列表 |
|
||||
| fleet[].vehicleType | Body | String | ✅ | 须能归一为 `suv/mpv/bus/sedan`(582022),归一后须在车队活字典内(**582032,本次新增**) | 车型大类,示例 `mpv`,可传中文别名(如"大巴") |
|
||||
| fleet[].seats | Body | Integer | ✅ | ≥0;须在该大类真实车型的座位选项内(582024) | 单车座位数 |
|
||||
| fleet[].count | Body | Integer | ✅ | >0 | 该车型数量 |
|
||||
| specialTags | Body | Array\<String\> | ❌ | 须在 `vehicle_special_demand` 字典内 | 通用特殊诉求 |
|
||||
| pickupRequired / dropoffRequired | Body | Boolean | ❌ | - | 接送机方向标记(兼容字段) |
|
||||
| remark | Body | String | ❌ | ≤500 字 | 备注 |
|
||||
|
||||
#### 出参 `Result<VehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 需求记录 ID |
|
||||
| kind | String | TRAVEL / TRANSFER |
|
||||
| version | Integer | 版本号 |
|
||||
| status | String | 需求状态 |
|
||||
| branchTaken | String | 本次实际走的分支:INIT_SUBMIT / PENDING_EDIT / DONE_ADJUST |
|
||||
| passengerCount | Integer | 出行人数 |
|
||||
| vehicleCount | Integer | 车辆总数 |
|
||||
| totalSeatCount | Integer | 总座位数 |
|
||||
| driverSeatCount | Integer | 司机座位数(= vehicleCount) |
|
||||
| passengerSeatCapacity | Integer | 载客座位容量 |
|
||||
| remainingPassengerSeats | Integer | 剩余可载客座位 |
|
||||
| previousVersion | Integer | DONE_ADJUST 分支下的上一版本号 |
|
||||
| assignmentDeletedCount | Integer | DONE_ADJUST 分支下被清理的派单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "TRAVEL",
|
||||
"fleet": [
|
||||
{ "vehicleType": "大巴", "seats": 7, "count": 1 }
|
||||
],
|
||||
"remark": "接机后直达酒店"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": 2098765432109876543,
|
||||
"kind": "TRAVEL",
|
||||
"version": 2,
|
||||
"status": "PENDING",
|
||||
"branchTaken": "PENDING_EDIT",
|
||||
"vehicleCount": 1,
|
||||
"totalSeatCount": 7
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空数据场景;提交成功即返回最新需求快照,无降级分支。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "branchTaken": "INIT_SUBMIT" }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`fleet[].vehicleType` 无法归一为 `suv/mpv/bus/sedan` 之一(含全部自由文本,如「别克GL8」)——**当前测试库数据下前端会实际撞上的错误**(存量码 582022,非本次新增,且排在校验链第一关):
|
||||
|
||||
```json
|
||||
{ "code": 582022, "message": "车型必须是车型大类(SUV/MPV/大巴/轿车)", "success": false, "data": null }
|
||||
```
|
||||
|
||||
本次新增两码,仅在 `vehicleType` 归一**成功之后**才有机会触发:
|
||||
|
||||
```json
|
||||
{ "code": 582032, "message": "车型 bus 不在车型字典内(不存在或已下线),请从下拉项中选择", "success": false, "data": null }
|
||||
```
|
||||
|
||||
582032 仅当归一得到的规范大类在车队活字典(`fleet_vehicle_type`)中已整类下线(该大类下所有型号均软删)时触发;测试库当前四个大类各有 1 行在架代表,本码在现有测试数据下不可达。
|
||||
|
||||
```json
|
||||
{ "code": 582033, "message": "车型字典不可用,请稍后重试", "success": false, "data": null }
|
||||
```
|
||||
|
||||
582033 仅当车队字典服务不可用(Feign 兜底返回空 Map)时触发,与本次具体填了什么车型无关,空字典按 fail-closed 处理。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:未登录 → 业务码 `401`。
|
||||
- 校验顺序:`fleet` 非空(582021)→ 逐项归一 + 座位数/数量非负校验(582022/582023)→ **车队活字典比对(582032/582033,本次新增)** → 座位选项校验(582024)→ 特殊诉求校验(582025/582026)。任一步失败即拒,零写入。
|
||||
- 存量已落库、含已下线大类的需求原样再次提交同一接口同样会被 582032 拒绝——这是有意设计(放行已下线大类等同于让下线动作失效),与团期批量车务 809119 同口径。
|
||||
- 车型下拉数据源固定为 `GET /admin/fleet/vehicle-types/list`(车队服务只读端点,网关已放行订单域只读调用,无需新增路由配置)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 调整订单统一提交 `POST /v3/admin/order/{id}/adjustment/submit`
|
||||
|
||||
**VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端订单调整弹窗统一提交入口;前端在内存里收集出行人/改期/行程/房需求/用车需求/接送机需求等多个子领域的改动后一次性提交。本次改动只影响 `updates.vehicleRequirement.fleet[]`(行程用车)与 `updates.transferRequirement.fleet[]`(接送机)两个子领域各自的车型大类校验,其余子领域字段与行为不受影响。
|
||||
|
||||
#### 入参
|
||||
|
||||
> 仅列与本次改动相关的字段;其余子领域(`people`/`schedule`/`itinerary`/`hotelRequirement` 等)字段结构未变,不在此重复列出。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| id | Path | Long | ✅ | 订单 ID | 目标订单 |
|
||||
| updates | Body | Object | ✅ | 至少一个子领域非 null | 各子领域修改内容容器 |
|
||||
| updates.vehicleRequirement.fleet | Body | Array\<FleetItem\> | ❌(该子领域非 null 时必传) | 非空则整组重新走校验链;结构与端点 1 的 `fleet` 完全相同 | 行程用车车型组合 |
|
||||
| updates.vehicleRequirement.fleet[].vehicleType | Body | String | 同端点 1 | 须归一成功(582022)且归一后在车队活字典内(**582032,本次新增**) | 车型大类 |
|
||||
| updates.transferRequirement.fleet | Body | Array\<FleetItem\> | ❌(该子领域非 null 时必传) | 与 `vehicleRequirement.fleet` 走同一条校验方法,服务日由大交通信息派生 | 接送机车型组合 |
|
||||
| updates.transferRequirement.fleet[].vehicleType | Body | String | 同端点 1 | 同上 | 车型大类 |
|
||||
|
||||
#### 出参 `Result<AdjustmentSubmitRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 提交是否成功;失败由全局异常处理器返回 `Result{code,message,data:null}`,不会出现该字段为 `false` 的情形 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"vehicleRequirement": {
|
||||
"fleet": [
|
||||
{ "vehicleType": "mpv", "seats": 7, "count": 1 }
|
||||
],
|
||||
"remark": "新增 2 名成员,需更大车型"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空数据场景;`updates` 内某子领域传 `null` 表示该子领域本次不改,不参与本次校验也不产生该子领域的变更记录。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
与端点 1 完全同码同序(`AdjustmentService.validateVehicleSeatOptionsBeforeWrite` 从 `updates.vehicleRequirement`/`updates.transferRequirement` 的 `fleet` 字段构造 `VehicleRequirementReqVO` 后,调用与端点 1 相同的 `validateVehicleRequirementSeatOptions`)。当前测试库数据下前端会实际撞上的同样是 582022:
|
||||
|
||||
```json
|
||||
{ "code": 582022, "message": "车型必须是车型大类(SUV/MPV/大巴/轿车)", "success": false, "data": null }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 582032, "message": "车型 bus 不在车型字典内(不存在或已下线),请从下拉项中选择", "success": false, "data": null }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 582033, "message": "车型字典不可用,请稍后重试", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:未登录 → 业务码 `401`。
|
||||
- `submit` 整体在一个 `@Transactional(rollbackFor = Exception.class)` 事务内;`updates.vehicleRequirement` 与 `updates.transferRequirement` 两个子领域各自独立校验一次,任一子领域校验失败即整单回滚,**已通过校验的其他子领域改动也不会落库**。
|
||||
- `updates.vehicleRequirement`/`updates.transferRequirement` 为 `null` 时该子领域完全跳过(既不校验也不改动),不受本次变更影响。
|
||||
- 存量已落库、含已下线大类的需求原样再次经本接口提交同样会被 582032 拒绝,同端点 1。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 从 `GET /admin/fleet/vehicle-types/list` 下拉选值提交 | `vehicleType` 取字典返回的规范 key(`suv/mpv/bus/sedan`) | 通过归一 + 字典校验 |
|
||||
| ✅ 提交中文别名 | `vehicleType: "大巴"` | 归一为 `bus` 后再查字典,与直接传 `bus` 等价 |
|
||||
| ❌ 自由文本/型号名 | `vehicleType: "别克GL8"` | 582022,归一失败,校验链第一关即拒 |
|
||||
| ❌ 大类已被车队整类下线 | `vehicleType: "bus"` 但活字典无 `bus` 在架代表 | 582032,归一成功但字典比对不通过 |
|
||||
| ❌ 车队字典服务不可用 | 任意合法 `vehicleType` | 582033,fail-closed,不做逐项放行 |
|
||||
|
||||
- `vehicleType` 一律以**归一后的规范 key**(非原始填法)落库;重新查看已提交需求时看到的也是规范 key,不是用户当时填的中文别名。
|
||||
- 端点 1(`vehicle-requirement`)与端点 2(`adjustment/submit`)中的 `fleet[]` 结构与校验规则完全一致,前端不需要为两个入口分别处理错误码。
|
||||
|
||||
### 前端已就绪的状态
|
||||
|
||||
mmg `v2.1` 分支 `7df292624`(2026-09-23 09:27)已将车型录入从自由文本改为 `GET /admin/fleet/vehicle-types/list` 字典下拉,新提交的 `vehicleType` 均取自字典返回值,不会触发本次新增的 582032/582033。
|
||||
|
||||
尚待前端覆盖的场景:**存量已落库需求的编辑态回显**——若某行 `vehicleType` 不在当前字典返回值内(历史遗留或大类刚被下线),下拉组件默认会渲染成空选项且用户无法感知原因;此时应显式提示"当前值不在字典内,请重新选择",而非静默清空。
|
||||
|
||||
**受影响的前端组件(实测点名,非推断)**:`src/views/order-v2/detail/modals/FunItemAdjustModal.vue`——hl-ui `v2.1` 上唯一调用 `putVehicleRequirement` 的 `.vue`(`:1177` import,`:2968` TRAVEL / `:2970` TRANSFER 两个调用点);api 封装在 `src/api/orderV2.js:1345` `putVehicleRequirement`。该组件已引入车型字典 api(`getVehicleTypesList`),本次要补的是「回显值不在字典返回集内」这一态的显式提示,不是接入下拉本身。
|
||||
|
||||
⚠️ 顺带一条可查证的事实:`src/api/orderV2.js` 中 `putVehicleRequirement` 的 JSDoc 错误码一行写的是 `582024 / 582091`,不含本次新增的 `582032 / 582033`。注释不影响运行,但下一个照注释排错的人会找不到新码。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无 Flyway migration、无 DDL、无表结构变更。
|
||||
- 本次校验是**写前置校验**,不涉及任何新增列或索引;`vehicle_requirement` 表落库字段与校验前完全一致(仅 `vehicle_type` 列存的值继续是归一后的规范 key)。
|
||||
- 校验失败(582022/582032/582033/582024 等)的请求**零写入**——包括 `vehicle_requirement` 主表、审计记录、派单联动,一律不产生任何行。
|
||||
- 车队活字典本身(`fleet_vehicle_type` 表)不因本次改动新增/修改任何行,字典维护仍走车队服务既有入口。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 业务码 `401`(网关包装 HTTP 200)。
|
||||
- `fleet` 为空数组或缺失 → 582021。
|
||||
- `vehicleType` 无法归一 → 582022(存量码,未变)。
|
||||
- `vehicleType` 归一成功但大类已整类下线 → **582032(本次新增)**。
|
||||
- 车队字典服务不可用(Feign 兜底返回空 Map)→ **582033(本次新增)**,对所有请求 fail-closed,不逐项放行。
|
||||
- `seats`/`count` 非法 → 582023;座位数不在该大类真实型号座位选项内 → 582024(582024 判定排在字典校验之后,因此大类已下线时不会先看到一个指错方向的座位错误)。
|
||||
- 存量已落库、含已下线大类的需求原样重新提交同一接口 → 同样被 582032 拒绝,零写入,不因"内容未变"豁免。
|
||||
- `adjustment/submit` 路径下,`validateVehicleTypesAgainstFleetDict` 内的 Feign 调用运行在 `AdjustmentService.submit` 的物理事务内(`PROPAGATION_REQUIRED` 挂入);这是该路径已有的事务边界,本次改动未改变这一行为,也不在本单修复范围。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### fleet[].vehicleType(车型大类规范 key)
|
||||
|
||||
**所属字段**: `VehicleRequirementReqVO.fleet[].vehicleType` / `AdjustmentSubmitReqVO.updates.vehicleRequirement.fleet[].vehicleType` / `...transferRequirement.fleet[].vehicleType` | **类型**: String
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `suv` | SUV |
|
||||
| `mpv` | MPV / 商务车 |
|
||||
| `bus` | 大巴 |
|
||||
| `sedan` | 轿车 |
|
||||
|
||||
- 归一表(含中文别名,如"大巴"→`bus`)由 `VehicleCategoryNormalizer` 维护,四值闭集,本次未新增第五类。
|
||||
- 实际**是否可选**取决于车队活字典当前有哪些大类在架,前端应以 `GET /admin/fleet/vehicle-types/list` 的实时返回为准,不要硬编码这四个 key 一定全部可用。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `fleet[].vehicleType` | 只要能归一为 `suv/mpv/bus/sedan` 之一即接受,不管该大类当前是否在车队还在营 | 归一成功后额外要求该大类在车队活字典内有 ≥1 行在架代表,否则拒 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 提交一个已被车队整类下线的大类(如运营已下线 `bus`) | 校验通过,正常落库 | 582032 拒绝,零写入 |
|
||||
| 车队字典服务不可用 | 不影响本校验链(该服务原本不参与车型大类校验) | 582033 拒绝,fail-closed |
|
||||
| 存量含已下线大类的需求原样重新提交 | 通过 | 582032 拒绝(与团期批量车务 809119 同口径) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 部分破坏,且是有意为之。请求/响应结构不变;仅"归一成功但大类已被下线"这一种此前被接受的取值,改为拒绝。影响面严格限定在"大类已整类下线"这一种运营主动下线的场景,不影响任何仍在营的大类。
|
||||
- **前端是否必须同步上线**: 否,mmg 已提前于本次后端上线(`7df292624` 早于 `b439565be`)改为字典下拉,新提交路径已规避新码。
|
||||
- **前端 workaround 清理点**: 存量已落库需求在编辑态若命中已下线大类,需前端显式提示"当前值不在字典内,请重新选择"(见"四、契约约束"节),避免下拉渲染成空白且用户无法定位原因。落点组件:`src/views/order-v2/detail/modals/FunItemAdjustModal.vue`。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: 两个端点中 `fleet[].vehicleType` 的字典比对这一步;其余校验(座位数、数量、特殊诉求、座位选项)逻辑不变。
|
||||
- **零影响**:
|
||||
- `VehicleCategoryNormalizer` 的四值归一表与别名规则本身(未新增/删减大类)。
|
||||
- `adjustment/submit` 中 `people`/`schedule`/`itinerary`/`hotelRequirement` 等与用车无关的子领域。
|
||||
- 团期批量车务(`GroupVehicleRequirementService`)校验链,809119/809120 已是同口径独立实现,本单未改动团级代码。
|
||||
- 派单、车队分配、司机端等下游链路的既有行为。
|
||||
- `GET /admin/fleet/vehicle-types/list` 端点本身(既有只读端点,本单未修改其实现或路由)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
单元测试(`RequirementServiceVehicleTypeDictValidateTest`):
|
||||
- `validateVehicleTypes_retiredCategoryRejectedAndLiveCategoryAccepted`:同一桩字典(刻意不含 `bus`)下,`大巴`(归一为 `bus`)被拒且断言错误码为 582032、非 582022;同一字典下 `suv` 正常放行。
|
||||
- `validateVehicleTypes_emptyDict_throws582033NotFail582032`:空字典(模拟车队服务不可用)下,`suv` 被拒且断言错误码为 582033、非 582032,报文不含用户填的车型值。
|
||||
|
||||
真实网关(TEST 环境)实证:
|
||||
|
||||
```
|
||||
PUT /v3/admin/order/{id}/vehicle-requirement vehicleType=__NOT_A_REAL_CATEGORY__ → 582022 ✓
|
||||
POST /v3/admin/order/{id}/adjustment/submit vehicleType=__NOT_A_REAL_CATEGORY__ → 582022 ✓
|
||||
```
|
||||
|
||||
两端点对同一非法值返回一致的错误码(582022),验证校验链在两个入口上等价生效、归一失败判定优先于字典比对。
|
||||
|
||||
部署: PR #8224 已合并 `dev-v3`(合并提交 `b439565be`),TEST 环境双实例滚动部署完成,Nacos 注册均 `healthy:true`。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8202](https://git.1814.love:8443/wx/HL/issues/8202)
|
||||
- 关联 PR: [wx/HL#8224](https://git.1814.love:8443/wx/HL/pulls/8224)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8202](https://git.1814.love:8443/wx/HL/issues/8202)
|
||||
- **PR**: [#8224](https://git.1814.love:8443/wx/HL/pulls/8224)
|
||||
- **Merge commit**: [b439565be](https://git.1814.love:8443/wx/HL/commit/b439565be)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8215"
|
||||
title: "团期详情(A2)新增出参 subOrderCount——活跃子订单户数,与 A3 total / 看板 orderCount 同源"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "团期详情页右上角显示「子订单 0 户」,同页「已建子订单 12 户」「整团名单速览(12 户)」却是 12。2026-09-23 对 TEST 团期 2101506167098511362 逐接口实测,后端四个接口无一返回 0:A2 详情 formingRooms/enrolledRooms/maxRooms 全 12、A3 子订单列表 total=12 且 records 有数据、看板 orderCount=12、财务 items 12 条。右上角三个金额(应收 156760 / 已收 47940 / 待收 108820)与 A2 的 receivableAmount/receivedAmount/unpaidAmount 逐字一致,说明该 UI 绑的就是详情响应对象——而详情原有的 50 个字段里没有任何子订单户数字段,前端取到 undefined 渲染成 0。subOrderCount 这个名字在 order-v3 里原本只存在于看板统计条 VO(GroupBatchSummaryVO,口径是命中筛选的全部团期活跃子订单合计,属列表页统计条)。本次后端兜底:A2 详情新增出参 subOrderCount,取数复用既有契约方法 OrderService#countActiveByProductBatchIds——A3 的 total 与看板 orderCount 走的都是它,口径同为 order_status != CANCELLED 加 @TableLogic 软删过滤,刻意不另写 count,避免「同屏两个数字不一致」换个形式复发。字段恒非 null,无活跃子订单返 0 而非 null。已合并 dev-v3(PR #8216,merge commit fc0508981)并部署测试服,09:50 三接口同轮实测同为 12。路径、入参、权限码、其余出参字段零变化,网关无改动。前端侧需确认该页取的就是 subOrderCount 这个字段名,故 frontend_status 记 pending。 mmg 2026-09-23 复核: 页面上那个 0 的取数点是 BatchHero orderCount 计算属性 `d.orderCount ?? d.subOrderCount ?? 0`(src/views/order-v2/batch/detail/components/BatchHero.vue),第二棒早已读 subOrderCount——A2 部署后该处即渲染真实户数,前端零行为改动,后端选的名字正好接上。已补注释+spec 两例锁回落链(hl-ui@61a549bd chore),判 not_required。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期详情(A2)新增出参 subOrderCount(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
|
||||
## 一、接口背景
|
||||
|
||||
团期详情页右上角同时渲染「成团状态 + 子订单户数 + 整团应收/已收/待收」。其中三个金额来自本接口,
|
||||
而「子订单户数」在本次之前**本接口并不返回**——前端取到 `undefined`,渲染成 `0`,
|
||||
与同页「已建子订单 12 户」「整团名单速览(12 户)」同屏打架。
|
||||
|
||||
本次在本接口补上该字段,取数与 A3 子订单列表、团期看板行**共用同一个契约方法**,三处必然同值。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | A2 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 修改接口 | 新增出参 `subOrderCount`,其余字段与行为零变化 |
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. A2 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
|
||||
|
||||
**VO**: `GroupBatchDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页进入时拉取整团概览:状态机阶段、成团闸/满员闸计数、四项 ready 标志、整团金额三项,
|
||||
以及本次新增的活跃子订单户数。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchId | path | Long | 是 | 正整数,团期聚合主键 | 团期 ID,非法或不存在返 GROUP_BATCH_NOT_FOUND |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| subOrderCount | Integer | **本次新增**。挂在本团期下的活跃子订单张数(户数)。口径:`order_main.product_batch_id` = 本团期 productBatchId 且 `order_status != CANCELLED`,软删由 `@TableLogic` 过滤。与 A3 子订单列表的 `total`、看板行的 `orderCount` 同一取数口径、同一契约方法,三处同值。恒非 null,无活跃子订单为 0 |
|
||||
| formingRooms | Integer | 成团判定用户数 = 线上已付款活跃订单数 + 产品域线下占位(#7287)。**与 subOrderCount 不是一回事**,有线下占位时必然大于后者 |
|
||||
| enrolledRooms | Integer | 已用房间数(既有字段,本次未改) |
|
||||
| receivableAmount | BigDecimal | 整团应收(既有字段,本次未改) |
|
||||
| receivedAmount | BigDecimal | 整团已收(既有字段,本次未改) |
|
||||
| unpaidAmount | BigDecimal | 整团待收 = max(0, 应收 − 已收)(既有字段,本次未改) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2101506167098511362 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2101506167098511362",
|
||||
"batchName": "jw测试1期",
|
||||
"opsStage": "FORMED",
|
||||
"opsStageName": "已成团",
|
||||
"subOrderCount": 12,
|
||||
"formingRooms": 12,
|
||||
"enrolledRooms": 12,
|
||||
"maxRooms": 12,
|
||||
"receivableAmount": "156760.00",
|
||||
"receivedAmount": "47940.00",
|
||||
"unpaidAmount": "108820.00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期存在但名下没有活跃子订单(全部 CANCELLED,或建团后尚未下单)时,`subOrderCount` 返 `0`,
|
||||
**不返 null、不缺字段**——null 在前端同样会渲染成空或 0,等于把本次要修的缺陷藏回去。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2101506167098511363",
|
||||
"opsStage": "RECRUIT",
|
||||
"subOrderCount": 0,
|
||||
"receivableAmount": "0.00",
|
||||
"receivedAmount": "0.00",
|
||||
"unpaidAmount": "0.00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期不存在",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 589501 | groupBatchId 不存在或已软删 |
|
||||
| 403 | 缺 `group-batch:view` 权限码,或定制师访问非自己归属的团期 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `subOrderCount` 只回答「这个团下面挂了几张活跃子订单」,**不含任何产品域线下占位**。
|
||||
- 已取消(CANCELLED)子订单不计入;软删由 `@TableLogic` 过滤,与 A3 缺省(`includeCancelled=false`)一致。
|
||||
- 与 A3 的 `total` 必然同值:两者调用同一个 `OrderService#countActiveByProductBatchIds` / 同一过滤条件。
|
||||
- 退单户(withdraw)在未置 CANCELLED 前仍计入,与 A3 行为一致。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 前端渲染「子订单 N 户」请取 `data.subOrderCount`,**不要取 `formingRooms`**(那是成团判定用数,含线下占位)。
|
||||
- 需要逐户明细时仍走 A3 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`,其分页包装为
|
||||
`{ records, total, page, pageSize }`——列表字段名是 `records`,不是 `list`。
|
||||
- 本字段是纯增出参,老调用方忽略它即可,无需改动。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
零数据库变更。本次不新增表/列/索引,不写任何数据;`subOrderCount` 的数据来源是既有列
|
||||
`order_main.product_batch_id` + `order_main.order_status` 的只读聚合,走既有契约方法,未新增 mapper 查询。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 团期不存在 → 589501,不返回半个对象。
|
||||
- 团期存在、无活跃子订单 → `subOrderCount: 0`(见「空数据 / 降级响应」)。
|
||||
- 团期下既有活跃单又有已取消单 → 只数活跃的,与 A3 缺省口径一致。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 出参字段数 | 50 | 51 |
|
||||
| `subOrderCount` | **不返回**(前端取到 undefined,页面渲染成 0) | 返回活跃子订单户数,恒非 null |
|
||||
| 其余出参字段 | — | 逐字未变 |
|
||||
| 路径 / 入参 / 权限码 | — | 逐字未变 |
|
||||
| 取数来源 | — | 复用 `OrderService#countActiveByProductBatchIds`(A3 与看板同款),未新增查询 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:纯增出参,老调用方忽略即可,无破坏性。
|
||||
- **性能**:详情装配内多一次按单个 productBatchId 的活跃单计数,走既有契约方法(A1 列表/看板/合并行三处已在用),
|
||||
单元素入参,无 N+1;落在既有 `@Transactional(readOnly = true)` 的库内装配段,不涉及 Feign。
|
||||
- **回滚**:撤销 PR #8216 即可,无数据与配置残留。
|
||||
- **未覆盖**:本次只证明后端返对了值。页面上那个 `0` 是否消失,取决于前端该处取的是不是
|
||||
`subOrderCount` 这个字段名——属前端侧确认项,`frontend_status` 记 `pending`。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- A3 团期下子订单列表、团期看板、团期财务总览:口径与字段零改动。
|
||||
- `formingRooms` / `enrolledRooms` / 金额三项:取数来源与数值零改动。
|
||||
- 网关:路径未变、无新增路由与权限码,`gateway_status: not_required`。
|
||||
- 小程序端:本接口仅管理后台使用,未涉及。
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
2026-09-23 09:50 测试服(api.test.1814.love:9443),团期 `2101506167098511362`(jw测试产品·第1期,已成团),
|
||||
部署 `dev-v3 @ fc0508981`,三接口**同轮**实测:
|
||||
|
||||
| 接口 | 字段 | 读数 |
|
||||
|---|---|---|
|
||||
| A2 团期详情 | `subOrderCount` | **12** |
|
||||
| A3 子订单列表 | `total` | **12** |
|
||||
| 团期看板 | `orderCount` | **12** |
|
||||
|
||||
同轮 `formingRooms=12`、`enrolledRooms=12`,与 `subOrderCount` 在本团期恰好同值(该团无线下占位)。
|
||||
|
||||
单元测试:`GroupBatchQueryServiceTest` 80 例 0 失败(新增 3 条,经 surefire XML 核实真执行,无 skipped),
|
||||
`GroupBatchQueryControllerTest` 9 例 0 失败,合计 89/0。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 工单 #8215、PR #8216(merge commit `fc0508981`)
|
||||
- `formingRooms` 口径出处:#7287
|
||||
- A3 分页包装形态(`records` 而非 `list`)出处:#7536
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:待确认该页取值字段名(`frontend_status: pending`)
|
||||
@@ -0,0 +1,483 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8218"
|
||||
title: "行程用车需求 PENDING_REVIEW 文案按 kind 分叉:TRAVEL「待提交车务」/ TRANSFER「待审核」"
|
||||
consumer: "admin"
|
||||
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: "【mmg 2026-09-24 判 not_required】grep 实证前端两出口均直显后端 *Name:RosterTable.vue:107(vehicleRequirementStatusName 三段回落)、VehicleHouseholdsSection.vue:112(statusName 直显,注释钉死「禁自建编码→中文映射」);文案分叉由后端下发自动生效,前端零映射零改动。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 行程用车需求状态文案分叉
|
||||
|
||||
> **存放目录**: `changelogs-v2/{YYYY-MM}/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3 (端口 8007)
|
||||
> **PR**: #8218
|
||||
> **Issue**: #8218
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台「团期查看需求」tab 下的子订单用车需求行状态文案
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**同一个状态码 `PENDING_REVIEW` 在两个出口里出现两种中文名**:行程用车(TRAVEL)记为「待提交车务」(等团期管理员整团放行,没有逐户审核动作),接送机(TRANSFER)保持「待审核」(有逐户审核动作)。这是体验分化的一部分,同一行程的不同需求类别流转节奏不同。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期用车需求分为两类:**行程用车(TRAVEL)** 由定制师逐户报,团期管理员整团一次审核并提交车务;**接送机(TRANSFER)** 由定制师逐户报,车务逐户审核。两条流程的「待审核」语义不同,混用会误导前端和用户。#8151 补齐了接送机字段后,#8218 据此修正了文案。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 响应字段值改变 | `vehicleRequirementStatusName` PENDING_REVIEW 场景分叉 |
|
||||
| 2 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 响应字段值改变 | `RequirementItem.statusName` PENDING_REVIEW 场景分叉 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
|
||||
|
||||
**VO**: `GroupBatchOrderItemRespVO → PageResult<GroupBatchOrderItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期详情」页面的「客户清单」tab,展示该团期下的全部子订单及其状态。前端需据 `vehicleRequirementStatusName` 在列表中展示该户的车需求进度。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期 ID |
|
||||
| page | Query | Integer | ❌ | ≥1 时取值,<1 归一为 1;缺省 1 | 页码 |
|
||||
| pageSize | Query | Integer | ❌ | 1-200;缺省 20,>200 截断为 200 | 每页条数 |
|
||||
| includeTravelers | Query | Boolean | ❌ | 缺省 true | 是否附出行人明细(证件号/手机号一律不返回) |
|
||||
| includeNeeds | Query | Boolean | ❌ | 缺省 true | 是否附房数/房型/特殊需求 |
|
||||
| includeCancelled | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | Long | 子订单 ID |
|
||||
| orderNo | String | 订单编号 |
|
||||
| customerName | String | 客户姓名 |
|
||||
| vehicleRequirementStatus | String | 车需求状态英文码(PENDING / PROCESSING / DONE / PENDING_REVIEW / REJECTED_TO_CONSULTANT / REJECTED_TO_ADMIN;无需求行回落 PENDING) |
|
||||
| vehicleRequirementStatusName | String | 车需求状态中文名;本列恒为行程用车(TRAVEL),故 PENDING_REVIEW 下发「待提交车务」而非「待审核」;无需求行回落「待车队配」 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /v3/admin/order/group-batch/2101506167098511362/orders?page=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"orderId": 2101506167043985410,
|
||||
"orderNo": "HL20260516143052999",
|
||||
"teamNo": "26-0001",
|
||||
"customerName": "王先生家庭",
|
||||
"participantCount": 4,
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"orderStatusName": "定制中",
|
||||
"payStatus": "DEPOSIT_PAID",
|
||||
"payStatusName": "已付定金",
|
||||
"vehicleRequirementStatus": "PENDING_REVIEW",
|
||||
"vehicleRequirementStatusName": "待提交车务",
|
||||
"consultantName": "张三",
|
||||
"totalPrice": "12000.00",
|
||||
"tierCode": "2A1C",
|
||||
"tierName": "2成人1儿童",
|
||||
"travelerInfoComplete": true
|
||||
}
|
||||
],
|
||||
"pageNumber": 1,
|
||||
"pageSize": 20,
|
||||
"totalCount": 45
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [],
|
||||
"pageNumber": 1,
|
||||
"pageSize": 20,
|
||||
"totalCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **本列恒为行程用车(TRAVEL)**:查询条件写死 `VehicleRequirementKind.TRAVEL`,所以子订单列表的 `vehicleRequirementStatusName` 永远不会出现「待审核」。这是 #8151 的既有设计,本次未改。
|
||||
- **纯接送机订单在此列的默认值**:定制师仅报了接送机需求而无行程用车需求的订单,在此出口读到的是后备 `vehicleRequirementStatus="PENDING"` / `vehicleRequirementStatusName="待车队配"`。若前端需区分这类订单的接送机状态,应改用出口二并传 `kind=TRANSFER`。
|
||||
- **无需求行回落**:未提交过任何行程用车需求的户,`vehicleRequirementStatus` 回落 `PENDING`,`vehicleRequirementStatusName` 回落「待车队配」。
|
||||
- **鉴权**:需 `AdminContextUtil` 权限检查(`GroupBatchPermissionGuard.PERMISSION_VIEW`)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
|
||||
|
||||
**VO**: `GroupVehicleHouseholdsRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期详情」页面的「查看需求」tab,展示该团期下逐户的用车需求明细。前端需据 `kind` 字段区分行程用车与接送机,并按其分别渲染对应的状态文案和审核流程。同一户可能同时有 TRAVEL 与 TRANSFER 两行,状态码与文案各自独立。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期 ID |
|
||||
| kind | Query | String | ❌ | TRAVEL / TRANSFER;不传则两类都返 | 需求类别过滤 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | Long | 团期 ID |
|
||||
| householdCount | Integer | 应报车的户数(按 orderId 去重,含一份需求都没提交的户) |
|
||||
| vehicleRowCount | Integer | 需求行数(一户可能 TRAVEL + TRANSFER 两行;未提交的户贡献 0 行,故本数可能小于 householdCount) |
|
||||
| households[].orderId | Long | 子订单 ID |
|
||||
| households[].customerName | String | 主联系人姓名 |
|
||||
| households[].status | String | 户级用车需求状态(null=该户一份需求都没提交;非 null 时取展示序首条的状态) |
|
||||
| households[].statusName | String | 户级用车需求状态中文名;PENDING_REVIEW 按首条需求行的 kind 分两套文案(TRAVEL=待提交车务 / TRANSFER=待审核) |
|
||||
| households[].requirements[].kind | String | 需求类别(TRAVEL / TRANSFER) |
|
||||
| households[].requirements[].kindName | String | 需求类别中文名 |
|
||||
| households[].requirements[].status | String | 需求状态编码(PENDING / PROCESSING / DONE / PENDING_REVIEW / REJECTED_TO_CONSULTANT / REJECTED_TO_ADMIN) |
|
||||
| households[].requirements[].statusName | String | 需求状态中文名;PENDING_REVIEW 按本行 kind 分两套文案(TRAVEL=待提交车务 / TRANSFER=待审核) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /v3/admin/order/group-batch/2101506167098511362/requirement/vehicle-households?kind=TRAVEL
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": 2101506167098511362,
|
||||
"departDate": "2026-10-01",
|
||||
"endDate": "2026-10-07",
|
||||
"householdCount": 3,
|
||||
"vehicleRowCount": 4,
|
||||
"countedHouseholdCount": 3,
|
||||
"households": [
|
||||
{
|
||||
"orderId": 2101506167043985410,
|
||||
"orderNo": "HL20260516143052999",
|
||||
"teamNo": "26-0001",
|
||||
"customerName": "王先生家庭",
|
||||
"participantCount": 4,
|
||||
"consultantName": "张三",
|
||||
"countedInSummary": true,
|
||||
"status": "PENDING_REVIEW",
|
||||
"statusName": "待提交车务",
|
||||
"requirements": [
|
||||
{
|
||||
"requirementId": 2101506167043985411,
|
||||
"kind": "TRAVEL",
|
||||
"kindName": "行程用车",
|
||||
"status": "PENDING_REVIEW",
|
||||
"statusName": "待提交车务",
|
||||
"fleet": [
|
||||
{
|
||||
"vehicleType": "suv",
|
||||
"vehicleTypeName": "SUV",
|
||||
"seats": 7,
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"specialTags": [],
|
||||
"remark": "需要儿童座椅",
|
||||
"serviceDates": [],
|
||||
"headcount": 4,
|
||||
"totalSeatCount": 7,
|
||||
"remainingPassengerSeats": 3,
|
||||
"pickupRequired": null,
|
||||
"dropoffRequired": null,
|
||||
"returnRemark": null,
|
||||
"returnedAt": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"orderId": 2101506167043985420,
|
||||
"orderNo": "HL20260516143052998",
|
||||
"teamNo": "26-0002",
|
||||
"customerName": "李女士一家",
|
||||
"participantCount": 3,
|
||||
"consultantName": "李四",
|
||||
"countedInSummary": true,
|
||||
"status": "PENDING",
|
||||
"statusName": "待车队配",
|
||||
"requirements": [
|
||||
{
|
||||
"requirementId": 2101506167043985421,
|
||||
"kind": "TRAVEL",
|
||||
"kindName": "行程用车",
|
||||
"status": "PENDING",
|
||||
"statusName": "待车队配",
|
||||
"fleet": [],
|
||||
"specialTags": [],
|
||||
"remark": null,
|
||||
"serviceDates": [],
|
||||
"headcount": 0,
|
||||
"totalSeatCount": 0,
|
||||
"remainingPassengerSeats": 0,
|
||||
"pickupRequired": null,
|
||||
"dropoffRequired": null,
|
||||
"returnRemark": null,
|
||||
"returnedAt": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": 2101506167098511362,
|
||||
"householdCount": 0,
|
||||
"vehicleRowCount": 0,
|
||||
"countedHouseholdCount": 0,
|
||||
"households": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **同一户可能混装两类需求**:同一屏上会并排出现「待提交车务」(TRAVEL 行)与「待审核」(TRANSFER 行)。这不是文案分叉的缺陷,两行的 `kind` 本来就不同、审核流程各自独立。前端需按 `kind` 字段分组渲染。
|
||||
- **户级状态(status/statusName)的定义**:户级状态取展示序首条(TRAVEL 优先)的状态。当同一户同时有 TRAVEL 与 TRANSFER 且两者状态码不同时(如 TRAVEL 已为 DONE、TRANSFER 还在 PENDING_REVIEW),户级 `statusName` 读到的是 TRAVEL 那条的文案。**逐条的权威状态一律读 `requirements[].status` 和 `requirements[].statusName`,不要读户级字段。**
|
||||
- **TRANSFER 专属字段**:`pickupRequired` 与 `dropoffRequired` 仅 `kind=TRANSFER` 时有值;`kind=TRAVEL` 时恒为 null(库里的值是写侧缺省 true,不表示该户要接机)。
|
||||
- **被打回的需求已失活**:打回后该行行号置为 REJECTED_* 且 `is_active=0`,不在本列表内。被打回的户在本列表里显示为 0 条需求行(#8151 遗留限定)。
|
||||
- **未提交需求的户**:户级 `status` 与 `statusName` 为 null,`requirements` 为空数组。
|
||||
- **不传 kind 参数时**:两类都返。这与提交侧「不传按 TRAVEL」的缺省刻意相反,防止接送机在页面上整类消失。
|
||||
- **鉴权**:需 `AdminContextUtil` 权限检查(`GroupBatchPermissionGuard.PERMISSION_VIEW`)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 状态码与文案的对应关系
|
||||
|
||||
| status | kind=TRAVEL | kind=TRANSFER | 说明 |
|
||||
|--------|------------|---------------|------|
|
||||
| PENDING | 待车队配 | 待车队配 | 待定制师报需求 |
|
||||
| PROCESSING | 配车中 | 配车中 | 定制师已报,车队配置中 |
|
||||
| DONE | 配车完成 | 配车完成 | 车队配置完毕 |
|
||||
| PENDING_REVIEW | **待提交车务** | **待审核** | **本次变化关键点**:TRAVEL 等团期管理员整团提交,TRANSFER 逐户审核 |
|
||||
| REJECTED_TO_CONSULTANT | 已驳回定制师 | 已驳回定制师 | 驳回给定制师修改 |
|
||||
| REJECTED_TO_ADMIN | 已驳回管理员 | 已驳回管理员 | 驳回给管理员修改 |
|
||||
|
||||
**前端注意**:不要用中文名做任何判定逻辑,一律用英文码 `status` 或 `vehicleRequirementStatus`;中文名仅用于展示。
|
||||
|
||||
### 调用顺序建议
|
||||
|
||||
出口一(子订单列表)适合快速获取列表页的概览信息;出口二(需求记录)适合进入详情页查看完整的车队配置。前端加载详情页时建议同时传 `kind` 参数筛选,而不是获取全量后在页面上过滤。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无写操作。响应字段 `vehicleRequirementStatusName` / `statusName` 由后端实时计算,读取 `order_vehicle_requirement.status` 列后由 `GroupBatchConverter.resolveRequirementStatusName(code, true, kind)` 翻译成中文。翻译逻辑中,`kind` 参数直接影响 PENDING_REVIEW 的输出。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **未登录**:401(网关拦截)
|
||||
- **团期不存在**:404
|
||||
- **无需求行**:出口一回落 `vehicleRequirementStatus="PENDING"` / `vehicleRequirementStatusName="待车队配"`;出口二该户出现 `requirements=[]` 且 `status/statusName=null`
|
||||
- **查询超时/服务降级**:不适用(纯查询,无外部依赖)
|
||||
- **权限不足**:403(后端权限检查失败)
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### vehicleRequirementStatus / status(需求状态)
|
||||
|
||||
**所属字段**: `GroupBatchOrderItemRespVO.vehicleRequirementStatus` / `GroupVehicleHouseholdsRespVO.HouseholdItem.status` / `GroupVehicleHouseholdsRespVO.RequirementItem.status` | **类型**: `String`
|
||||
|
||||
| 值 | TRAVEL 文案 | TRANSFER 文案 | 说明 |
|
||||
|----|-----------|-------------|------|
|
||||
| PENDING | 待车队配 | 待车队配 | 定制师未提交或提交后被清空 |
|
||||
| PROCESSING | 配车中 | 配车中 | 定制师已提交,车队正在配置 |
|
||||
| DONE | 配车完成 | 配车完成 | 车队配置已完毕 |
|
||||
| PENDING_REVIEW | 待提交车务 | 待审核 | #8218 分叉。TRAVEL:等团期管理员整团放行;TRANSFER:逐户审核 |
|
||||
| REJECTED_TO_CONSULTANT | 已驳回定制师 | 已驳回定制师 | 审核打回给定制师修改 |
|
||||
| REJECTED_TO_ADMIN | 已驳回管理员 | 已驳回管理员 | 审核打回给管理员修改 |
|
||||
|
||||
### kind(需求类别)
|
||||
|
||||
**所属字段**: `GroupVehicleHouseholdsRespVO.RequirementItem.kind` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| TRAVEL | 行程用车 | 出团期间的长距离用车,由定制师逐户报,团期管理员整团审核 |
|
||||
| TRANSFER | 接送机 | 出发地/目的地的往返机场/火车站接送,由定制师逐户报,车务逐户审核 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 | 变化说明 |
|
||||
|------|------|------|----------|
|
||||
| `GroupBatchOrderItemRespVO.vehicleRequirementStatusName` | PENDING_REVIEW 时固定「待审核」| PENDING_REVIEW 时下发「待提交车务」(本出口恒为 TRAVEL) | 新增第 3 参 `vehicleKind` 到转换函数,按 kind 分叉 |
|
||||
| `GroupVehicleHouseholdsRespVO.HouseholdItem.statusName` | PENDING_REVIEW 时固定「待审核」| PENDING_REVIEW 时按首条需求行的 kind 分叉 | 同步调整 |
|
||||
| `GroupVehicleHouseholdsRespVO.RequirementItem.statusName` | PENDING_REVIEW 时固定「待审核」| PENDING_REVIEW 时按本行 kind 分叉 | 同步调整 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 | 影响 |
|
||||
|------|------|------|------|
|
||||
| TRAVEL PENDING_REVIEW 文案 | 「待审核」 | 「待提交车务」 | 前端渲染改变,需同步 UI 逻辑 |
|
||||
| TRANSFER PENDING_REVIEW 文案 | 「待审核」 | 「待审核」 | 无改变 |
|
||||
| 查询接口无新增参数 | - | 出口二仍支持 `kind` 筛选,无新参数 | 前端无需改入参 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。返回字段名、字段类型、入参名均无改变,仅字段值的中文描述改变(可视为前端展示层改变,不是数据契约改变)。
|
||||
- **前端是否必须同步上线**: 是。出口一的文案变化直接影响子订单列表的渲染;出口二的文案变化影响需求详情页的渲染。前端若依赖旧的「待审核」文案做 UI 逻辑判定(如条件渲染、样式选择),将显示错误。
|
||||
- **前端 workaround 清理点**:
|
||||
- 原有「TRAVEL 需求在 PENDING_REVIEW 时文案为「待审核」」的假设可全部删除
|
||||
- 原有「TRANSFER 需求在 PENDING_REVIEW 时文案为「待审核」」的假设仍然有效
|
||||
- 若原代码中写死了状态文案映射(如 `const statusMap = { PENDING_REVIEW: '待审核' }`),需改为按 `kind` 分叉的版本
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:
|
||||
- 管理后台「团期详情」页面的「客户清单」tab(出口一)
|
||||
- 管理后台「团期详情」页面的「查看需求」tab(出口二)
|
||||
- 以上两处 PENDING_REVIEW 状态的文案展示
|
||||
|
||||
- **零影响**:
|
||||
- C 端订单详情(不调用这两个端点)
|
||||
- 订单创建/支付/确认接口
|
||||
- 用房需求接口(独立体系)
|
||||
- 子订单逐户审核/打回/提交流程的后端逻辑(本变更仅涉文案翻译,无业务流程改动)
|
||||
- 历史订单数据(读取时即时翻译,无存量迁移)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
测试服部署:`hl-order-service-v3` / branch `dev-v3` / commit `7738668b8` / 2026-09-23 13:34:56
|
||||
|
||||
**出口一验证**
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/2101009024268386304/orders?page=1&pageSize=20
|
||||
vehicleRequirementStatus=PENDING_REVIEW, kind=TRAVEL (隐含)
|
||||
→ vehicleRequirementStatusName=「待提交车务」 ✓
|
||||
```
|
||||
|
||||
**出口二验证 - TRAVEL**
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/2101009024268386304/requirement/vehicle-households?kind=TRAVEL
|
||||
HouseholdItem.requirements[kind=TRAVEL].status=PENDING_REVIEW
|
||||
→ HouseholdItem.requirements[kind=TRAVEL].statusName=「待提交车务」 ✓
|
||||
```
|
||||
|
||||
**出口二验证 - TRANSFER**
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/2101009024268386304/requirement/vehicle-households?kind=TRANSFER
|
||||
RequirementItem[kind=TRANSFER].status=PENDING_REVIEW
|
||||
→ RequirementItem[kind=TRANSFER].statusName=「待审核」 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8218](https://git.1814.love/wx/HL/issues/8218)
|
||||
- 后续计划: 前端改动由 mmg 团队负责
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8218](https://git.1814.love/wx/HL/issues/8218)
|
||||
- **PR**: [#8218](https://git.1814.love/wx/HL/pulls/8218)
|
||||
- **后端 commit**: [7738668b8](https://git.1814.love/wx/HL/commit/7738668b8)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,407 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8220"
|
||||
title: "团期正式行程用车需求自动汇总草稿"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ee98b96654fd250159ee4a3b0067e5f1c88d3b75"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "后端已部署 TEST 并经网关实测;待 mmg 在编辑弹窗接入「自动汇总」+ 乘车户全选 + 人数随所选户汇总;前端已交付:编辑弹窗「自动汇总」灌草稿+四诊断展示+大类收敛(suv→suv2)+version 用草稿值+非 DRAFT 禁保存;spec 41 例全绿,checkpoint 13 项过"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期正式行程用车需求自动汇总草稿(#8220)
|
||||
|
||||
> **服务**: hl-order-service-v3(经网关调用,无需关心服务端口)
|
||||
> **PR**: #8273
|
||||
> **Issue**: #8220
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情「用车」Tab 的正式行程用车需求编辑弹窗
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
新增只读端点,按各子订单已提交的行程用车(TRAVEL)需求,自动汇总出团级正式用车需求草稿。草稿形状与保存端点 `PUT .../vehicle-requirement` 的请求体**完全一致**,前端可以直接灌进编辑弹窗、原样保存。
|
||||
|
||||
**四条会直接影响你怎么写代码的点,按严重度排**:
|
||||
|
||||
1. 🔴 **草稿之外的四个诊断字段必须展示,不能只看 `draft`**。`droppedFleetItems` 里是**没进草稿的车型需求**(一户报了多个车型时只保留主车型),不提示的话,管理员一保存,这些需求就从团级正式需求里消失了。另外三个字段(`staleHeadcountOrders` / `paddedOrderDays` / `violations`)见业务边界。
|
||||
2. 🔴 **车型回显是归一后的大类 key(`suv` / `bus` / `mpv` / `sedan`)**,与 #8221 交接件里提过的是同一个问题:车型下拉的 value 是 fleet typeKey `suv2`,拿 `suv` 逐字去比会判成「非字典值,请重选」,并被前端校验拦住保存。**判「是否字典值」要按归一后的大类比**,否则所有 SUV 组汇总出来都会被弹窗拦下。
|
||||
3. **有户还没提交行程用车需求时,本端点直接返回业务错误 `809121`**,报文逐户列出「订单号(orderId):原因」。这时不要尝试拼一份草稿,应提示管理员去催这些户。
|
||||
4. **成功码是 `code: 200`**;业务失败也返回 HTTP 200,**一律按 `body.code` 判**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
wx 2026-09-23 团期详情页「用车」Tab 反馈:「正式的行程用车需求要根据子订单的行程用车需求自动汇总 自动填写」,以及「得有个全选的按钮,根据选的乘车户自动把人数算出来」。此前编辑弹窗不拉取任何子订单需求数据,日期预填的是团期自己的出发/结束日,其余字段全靠手填。汇总规则由管理者于 2026-09-23 定案(#8220 评论)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 新增接口 | 只读,返回可原样保存的草稿与汇总诊断 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理员在编辑弹窗里点「自动汇总」时调用。拿到 `data.draft` 后填进弹窗,同时展示四个诊断字段;管理员确认后,把 `draft` 原样(或编辑后)交给 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 保存。
|
||||
|
||||
弹窗里如果已经有编辑内容,点「自动汇总」前请二次确认「这将覆盖当前编辑内容」。本端点不管现在有没有正式需求,都返回完整汇总,要不要覆盖由前端交互决定。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期 ID(`group_batch_id`),🔴 不是产品班期 ID |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `groupBatchId` | String(雪花 ID) | 团期 ID |
|
||||
| `currentStatus` | String / null | 当前正式需求状态;还没形成时为 null。**只有 null 或 `DRAFT` 时才能保存**(其余状态保存会报 809101 / 809115),前端据此决定「保存」按钮是否可用 |
|
||||
| `draft` | Object | 汇总草稿,与保存请求体同形,**恒非 null** |
|
||||
| `draft.version` | Integer / null | 当前正式需求的乐观锁版本;未形成时为 null。保存时原样带上 |
|
||||
| `draft.remark` | String / null | 整份备注,汇总时恒为 null |
|
||||
| `draft.groups[]` | Array | 乘车分组,没有可汇总内容时为空数组 |
|
||||
| `draft.groups[].groupId` | Long / null | 恒为 null(汇总产物一律是新组) |
|
||||
| `draft.groups[].groupCode` | String | 车型大写,同车型按连续日期段拆组时第二段起加序号:`BUS`、`BUS2`… |
|
||||
| `draft.groups[].vehicleType` | String | 归一后的车型大类 key:`bus` / `suv` / `mpv` / `sedan` |
|
||||
| `draft.groups[].serviceStartDate` | String(`yyyy-MM-dd`) | 本组首日 |
|
||||
| `draft.groups[].serviceEndDate` | String(`yyyy-MM-dd`) | 本组末日 |
|
||||
| `draft.groups[].seats` | Integer / null | 组内各户保留车型项里最大的单车座位数;各户都没填座位时,与 `count` 一起为 null |
|
||||
| `draft.groups[].count` | Integer / null | `ceil(本组最忙那天的人数 / (seats − 1))`,已扣除司机座 |
|
||||
| `draft.groups[].specialTags` | String[] | 组内各户特殊诉求编码的并集(去重、保持顺序) |
|
||||
| `draft.groups[].remark` | String / null | 逐户「订单号: 定制师备注」和「订单号 另报 suv 7座×1」拼接而成,超过 500 字截断。**仅供人眼留底** |
|
||||
| `draft.groups[].days[]` | Array | 逐日明细,正好铺满本组首日到末日 |
|
||||
| `draft.groups[].days[].tripDate` | String(`yyyy-MM-dd`) | 日期 |
|
||||
| `draft.groups[].days[].headcount` | Integer | 当天在组各户的**实时**人数之和 |
|
||||
| `draft.groups[].days[].memberOrderIds` | String[](雪花 ID) | 当天在组的子订单 ID。🔴 19 位雪花 ID,前端一律按字符串处理 |
|
||||
| `droppedFleetItems[]` | Array | 没进草稿的车型项,见业务边界第 1 条 |
|
||||
| `droppedFleetItems[].orderId` / `orderNo` | String / String | 所属子订单 |
|
||||
| `droppedFleetItems[].vehicleType` / `seats` / `count` | String / Integer / Integer | 被丢弃项(子订单原值) |
|
||||
| `droppedFleetItems[].keptVehicleType` | String | 该户被归入的主车型 |
|
||||
| `droppedFleetItems[].reason` | String | `NOT_PRIMARY_TYPE`(非主车型)/ `VEHICLE_TYPE_NOT_IN_DICT`(车型不在车型字典内) |
|
||||
| `staleHeadcountOrders[]` | Array | 实时人数与子订单需求提交时冻结的人数不一致的户:`orderId` / `orderNo` / `frozenHeadcount` / `liveHeadcount` |
|
||||
| `paddedOrderDays[]` | Array | 为了覆盖该户「出发~返回」每一天而补进分组、但不在该户行程用车服务日里的日期:`orderId` / `orderNo` / `dates[]` |
|
||||
| `violations[]` | Array | 草稿按保存时同一套校验预检出的问题:`code` / `reason` / `detail` / `groupCode` / `tripDate` / `orderId`。正常应为空数组 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement/aggregate-draft
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102692937584513025",
|
||||
"currentStatus": null,
|
||||
"draft": {
|
||||
"version": null,
|
||||
"remark": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2026-11-24",
|
||||
"serviceEndDate": "2026-11-26",
|
||||
"seats": 16,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "HL20260923173356872: 8220 甲户多车型;HL20260923173356872 另报 suv 5座×1;HL20260923173401731: 8220 乙户",
|
||||
"days": [
|
||||
{
|
||||
"tripDate": "2026-11-24",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-25",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-26",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "SUV",
|
||||
"vehicleType": "suv",
|
||||
"serviceStartDate": "2026-11-24",
|
||||
"serviceEndDate": "2026-11-26",
|
||||
"seats": 5,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "HL20260923173405165: 8220 丙户",
|
||||
"days": [
|
||||
{
|
||||
"tripDate": "2026-11-24",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-25",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-26",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"droppedFleetItems": [
|
||||
{
|
||||
"orderId": "2102692937378992129",
|
||||
"orderNo": "HL20260923173356872",
|
||||
"vehicleType": "suv",
|
||||
"seats": 5,
|
||||
"count": 1,
|
||||
"keptVehicleType": "bus",
|
||||
"reason": "NOT_PRIMARY_TYPE"
|
||||
}
|
||||
],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**示例说明**:示例值取自测试环境一次真实调用,不构成可复现夹具。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团里没有需要用车的户,也没有任何 TRAVEL 需求时,返回空草稿(**`groups` 是空数组,不是 null**):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102692937584513025",
|
||||
"currentStatus": null,
|
||||
"draft": { "version": null, "remark": null, "groups": [] },
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
车型字典(车队服务)不可用时**不降级**,返回 809120,见错误响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**有户缺少可汇总的行程用车需求**(未提交 / 被打回未重提 / 车型都不在字典内 / 推不出日期 / 人数为 0):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 2102692937584513025 有 1 户缺少可汇总的行程用车需求,暂不能自动汇总:HL20260923173405165(2102692971709370370):未提交行程用车需求",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**车型字典暂不可用**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809120,
|
||||
"message": "车队车型字典暂不可用,无法校验车型,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**无权限**(当前角色没有 `group-batch:demand:confirm`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **多车型户**:一户的行程用车同时报了多个车型(如 bus×1 + suv×1)时,这一户只进「主车型」组。主车型取座位数×辆数最大的那一项,相等时取车型编码字典序小的,所以同一份数据每次汇总结果都相同。其余车型进 `droppedFleetItems`,🔴 **前端必须醒目提示**「以下车型需求未包含在草稿中」。这样做是因为同一户同一天只能属于一个分组(809108)。
|
||||
- **拆组**:同一车型的日期如果断开(比如甲 10-08~10-09、乙 10-12~10-13),会拆成 `BUS` / `BUS2` 两组,不会产出某天零人的逐日行。
|
||||
- **补日**:团级保存要求每个需车户「出发~返回」的每一天都被分组覆盖(809109)。子订单行程用车的服务日如果比这个范围窄,多出来的日期也会把该户算进车,并列在 `paddedOrderDays` 里。前端提示「以下户在这些日期原本未报用车,已按行程补入」。
|
||||
- **人数**:逐日人数用订单**实时**人数,与户列表显示的人数一致。实时人数与子订单需求提交时冻结的人数不一致时,该户进 `staleHeadcountOrders`,说明那户的子订单用车需求已经过期,车务在子订单侧看到的还是旧值。
|
||||
- **座位与车数**:`count` 按扣掉司机座的口径算,所以草稿在团级(809116)和子订单级两种座位校验下都成立。
|
||||
- **`violations` 为空也不保证保存一定成功**:`currentStatus` 不是 null 或 `DRAFT`、或者汇总之后有人改过正式需求(version 变了),保存照样会被拒。
|
||||
- **只汇总行程用车(TRAVEL)**,接送机(TRANSFER)不进团车。
|
||||
- **本端点零写入**:不取锁、不进事务,调多少次都不影响数据。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误请求对照
|
||||
|
||||
| 场景 | 请求 | 预期(HTTP 恒 200) |
|
||||
|------|------|------|
|
||||
| ✅ 各户都已提交 | `GET .../{groupBatchId}/vehicle-requirement/aggregate-draft` | `code: 200`,`draft.groups` 非空 |
|
||||
| ✅ 原样保存 | `PUT .../{groupBatchId}/vehicle-requirement`,请求体 = `data.draft` | `code: 200` |
|
||||
| ❌ 有户未提交 | 同上 GET | `code: 809121`,报文列出未提交的户 |
|
||||
| ❌ 路径传成产品班期 ID | `GET .../{productBatchId}/vehicle-requirement/aggregate-draft` | 团期不存在的错误码(589500) |
|
||||
|
||||
### 前端「乘车户全选 + 人数自动汇总」的取数口径(wx 反馈第 ③ 条)
|
||||
|
||||
- **户清单与每户人数**:用既有的 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRAVEL`,人数取 `households[].participantCount`。它与本端点草稿里的逐日人数同源,都是订单实时人数。
|
||||
- 🔴 **不要用分页的团期订单列表做「全选」**:那个列表单页上限 200,户数超过 200 时会漏选。`vehicle-households` 不分页,单次最多 500 户。
|
||||
- **某天的人数** = 当天勾选各户的 `participantCount` 之和。汇总草稿里的 `days[].headcount` 就是按这个口径算的,可以直接对照。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本接口**只读**,没有任何数据库写入,不新增表、不改表结构,也没有 Flyway 脚本。测试环境实测:调用前后,团级正式需求相关表与子订单用车需求表的行数和 `update_time` 都没有变化(见第八节)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **鉴权**:需要 `group-batch:demand:confirm`,与 `GET/PUT .../vehicle-requirement` 同一个权限码。不带 token 时网关返回 401;角色没有该权限时返回 `code: 589507`。
|
||||
- **网关路由**:沿用现有的 `- Path=/v3/admin/**` → `lb://hl-order-service-v3`,本单**不新增路由**。
|
||||
- **团期不存在** → 589500。
|
||||
- **有户缺少可汇总需求** → 809121;**车型字典不可用** → 809120。
|
||||
|
||||
---
|
||||
|
||||
## 六.5 枚举 / 数据字典
|
||||
|
||||
### droppedFleetItems[].reason
|
||||
|
||||
**所属字段**: `droppedFleetItems[].reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `NOT_PRIMARY_TYPE` | 非主车型 | 该户多车型,只保留主车型 |
|
||||
| `VEHICLE_TYPE_NOT_IN_DICT` | 车型不在字典 | 车型归一后不在车队车型字典内 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6 修改前后对比
|
||||
|
||||
无,本接口为新增。
|
||||
|
||||
---
|
||||
|
||||
## 六.7 影响评估
|
||||
|
||||
- **破坏兼容**:否,新增接口
|
||||
- **前端同步上线要求**:否,不接入也不影响现有编辑与保存流程
|
||||
- **新增错误码**:`809121`(有户缺少可汇总的行程用车需求)
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:团期正式行程用车需求编辑弹窗的「自动汇总」入口
|
||||
- **零影响**:
|
||||
- 正式用车需求的保存 / 读取 / 撤回 / 免车 / 确认(校验语义一处未改)
|
||||
- 子订单用车需求的提交与审核
|
||||
- 接送机(TRANSFER)链路
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关调用(`https://api.test.1814.love`),hl-order-service-v3 已部署 `dev-v3 @ 9b60bc62b`(本单合并提交,两个实例都在 17:21 滚动重启)。夹具全部自建:产品班期 `2102692899017912323`,团期 `2102692937584513025`,三户订单甲 / 乙 / 丙。
|
||||
|
||||
```
|
||||
GET .../2102692937584513025/vehicle-requirement/aggregate-draft (三户都没提交 TRAVEL)
|
||||
→ 809121,报文列出 3 户 ✓;甲户提交后列 2 户 ✓;乙户提交后列 1 户 ✓
|
||||
GET .../aggregate-draft (三户都已提交:甲 bus 16×1 + suv2 5×1、乙 bus 12×1、丙 suv2 5×1)
|
||||
→ 200:BUS 组(甲 + 乙,seats=16,count=1,每天 5 人)+ SUV 组(丙);
|
||||
droppedFleetItems = 甲户 suv 5×1(NOT_PRIMARY_TYPE);violations = [] ✓
|
||||
PUT .../2102692937584513025/vehicle-requirement 请求体 = data.draft 原样
|
||||
→ 200,version=1;GET 回读后逐字段比对与草稿一致 ✓(再汇总一次、带 version=1 原样保存 → 200,version=2 ✓)
|
||||
零写入:调汇总前后各读一次 order_group_vehicle_requirement / _group / _group_day / order_vehicle_requirement
|
||||
的全表行数与 MAX(update_time),以及本团相关行 → 两轮前后完全一致 ✓
|
||||
丙户另外提交了 TRANSFER(mpv,11-23 接机)→ 草稿里没有 mpv,也没有 11-23 ✓
|
||||
不带 token → code 401「缺少有效的 Authorization 头」 ✓
|
||||
定制师角色(无 group-batch:demand:confirm)→ code 589507 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #8221(车型字典与存量车型归一):本端点输出的车型同样要通过 809119
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8220](https://git.1814.love/wx/HL/issues/8220)
|
||||
- 关联 PR: [wx/HL#8273](https://git.1814.love/wx/HL/pulls/8273)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8220](https://git.1814.love/wx/HL/issues/8220)
|
||||
- **PR**: [#8273](https://git.1814.love/wx/HL/pulls/8273)
|
||||
- **Merge commit**: [9b60bc62b](https://git.1814.love/wx/HL/commit/9b60bc62b)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8221"
|
||||
title: "团期正式用车需求: 存量自由文本车型已归一成字典大类 key,原样回传不再撞 809119;前端 SUV 下拉回显需按大类比对"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修复"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c314c8c67ddc61b8a1e4f4373d79e5a731b13272"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "接口契约零变化(路径、入参、出参、错误码都没改),变的是库里存量数据:Java 迁移 V20260924_402 把活跃版本上 81 行自由文本车型归一成 suv/mpv/sedan/bus。PR #8236 已合并 dev-v3(f5ffeffe7);TEST order-v3 在 fe752f16c(含本单)上运行,迁移 2026-09-23 12:19:36 执行成功。经真实网关验证:字典 4 个 typeKey 都过了车型校验,minivan 仍报 809119,存量团期原样回传返回 200。gateway_status=verified:零新增路由,在既有端点上做了 round-trip。frontend_status=pending:GroupVehicleRequirementEditModal.vue 判「是否字典值」时拿原值和 typeKey 逐字比,SUV 组回显 suv 对不上 suv2,见正文第三节。 | 2026-09-23 mmg 交付:EditModal 按 vehicleTypeName 匹配字典 typeName 收敛回显,不占位不拦保存"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期正式用车需求: 存量车型归一 + SUV 下拉回显不匹配
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(Flyway Java 迁移)
|
||||
> **PR**: [#8236](https://git.1814.love:8443/wx/HL/pulls/8236)
|
||||
> **Issue**: [#8221](https://git.1814.love:8443/wx/HL/issues/8221)
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: `GET` / `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 里 `groups[].vehicleType` 的**存量取值**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **存量团期的 `groups[].vehicleType` 现在都是规范大类 key**(`suv` / `mpv` / `sedan` / `bus`),不再有 `35座大巴`、`SUV`、`jiaoche` 这类自由文本。
|
||||
所以打开存量团期的编辑弹窗、不改车型直接保存,**不会再撞 809119**。
|
||||
2. **`groups[].vehicleTypeName` 对这些行现在有值了**(如 `大巴系列`)。以前原值不是 key,取不到中文名,返回 null。
|
||||
3. **归一时丢掉的原文(座位数、拼音写法)追加在该组 `groups[].remark` 里**,格式为 `原车型:35座大巴`;原来有备注的,用 `;` 接在后面。
|
||||
4. **校验本身没有放宽**:字典外的值(如 `minivan`)照样报 809119,文案不变。
|
||||
|
||||
---
|
||||
|
||||
## 一、变更接口清单
|
||||
|
||||
| 方法 | 路径 | 变更性质 |
|
||||
|---|---|---|
|
||||
| GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 字段结构不变,存量 `vehicleType` 取值已归一 |
|
||||
| PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 契约不变;存量值原样回传不再被 809119 拒 |
|
||||
|
||||
> 本条不属于「新增接口 / 修改接口」:请求参数、响应字段、错误码都没有增删改。
|
||||
|
||||
---
|
||||
|
||||
## 二、存量映射表(TEST 实测,活跃版本)
|
||||
|
||||
| 原值 | 归一后 | 行数 | remark 留底 |
|
||||
|---|---|---|---|
|
||||
| `SUV` | `suv` | 35 | 否 |
|
||||
| `35座大巴` | `bus` | 15 | 是 |
|
||||
| `轿车` | `sedan` | 13 | 否 |
|
||||
| `商务车` | `mpv` | 10 | 否 |
|
||||
| `jiaoche` | `sedan` | 4 | 是 |
|
||||
| `35zuo daba` | `bus` | 1 | 是 |
|
||||
| `7座商务车` | `mpv` | 1 | 是 |
|
||||
| `car` | `sedan` | 1 | 是 |
|
||||
| `shangwu` | `mpv` | 1 | 是 |
|
||||
|
||||
- 只改**当前活跃版本**。历史版本(已失效的需求版本)保持原样,所以查历史时仍可能看到旧的自由文本。
|
||||
- 迁移认不出的值会原样保留(TEST 活跃版本里为 0 个),这类值仍会被 809119 拒,需要在下拉里重选。
|
||||
- `update_time` 没有被刷新,这次归一不会显示成一次用户编辑。
|
||||
|
||||
---
|
||||
|
||||
## 三、🔴 前端需要改的一处:SUV 组存完再打开会被判成「非字典值」
|
||||
|
||||
**现象**:`GroupVehicleRequirementEditModal.vue` 的下拉 value 用的是 `GET /admin/fleet/vehicle-types/list` 的 `typeKey` 原值。TEST 上 SUV 这一项的 `typeKey` 是 **`suv2`**。
|
||||
后端保存时会把提交值归一成大类 key 再落库(`suv2` → `suv`),GET 也原样回显 `suv`。
|
||||
|
||||
`vehicleTypeKeys` 只有 `{suv2, mpv, sedan, bus}`,于是:
|
||||
|
||||
- `vehicleTypeOptionsFor(g)`(`:306-316`)会给这组合成一个 `suv(非字典值,请重选)` 占位项;
|
||||
- `vehicleTypeRule`(`:343-355`)把这组判成字典外的值,**拦住保存**。
|
||||
|
||||
⇒ **只要是 SUV 组,每次打开编辑弹窗都得重选一遍**。本次归一后,存量 35 行 `SUV` 都变成了 `suv`,也会落进这个情况。`mpv` / `sedan` / `bus` 的 typeKey 与大类 key 正好相同,不受影响。
|
||||
|
||||
**建议改法**(选一种):
|
||||
|
||||
- 判断「当前值是否在字典内」时按**大类**比较:GET 回显的 `vehicleTypeName` 非空,就说明该值已命中字典大类,可以用它去匹配下拉项的 `typeName`(TEST 上 `suv` → `SUV系列`,与 `suv2` 的 `typeName` 相同);
|
||||
- 或者在前端维护一张 typeKey → 大类的映射,用来选中回显项。**不要写死 `suv2`**:字典是运行期数据,typeKey 是开集,随时可能变。
|
||||
|
||||
提交时继续传 typeKey 原值(`suv2`),后端会自己归一,这一点不变。
|
||||
|
||||
---
|
||||
|
||||
## 四、后端已验证的读数(测试服活体,order-v3 @ `fe752f16c`)
|
||||
|
||||
| 验证项 | 读数 |
|
||||
|---|---|
|
||||
| 迁移执行 | `flyway_schema_history` 版本 `20260924.402` success=1,2026-09-23 12:19:36,耗时 11ms |
|
||||
| 数据终态 | 活跃版本 83 行全部是规范 key;23 行 remark 带「原车型:」;历史版本 19 行没有动 |
|
||||
| 字典 typeKey 全部能过 | `suv2` / `mpv` / `sedan` / `bus` 逐个 PUT,都没触发 809119 |
|
||||
| 阴性对照 | `minivan` → `809119 第 ALL 组的车型 minivan 不在车型字典内(不存在或已下线),请从下拉项中选择` |
|
||||
| 存量原样回传 | 原值为 `35座大巴` 的团期:GET 回显 `bus` / `大巴系列`,原样 PUT → HTTP 200 / code 200,version 2→3 |
|
||||
| 鉴权 | 不带 token → 401 |
|
||||
|
||||
---
|
||||
|
||||
## 五、你需要做的
|
||||
|
||||
- 按第三节改一下 `GroupVehicleRequirementEditModal.vue` 判断字典值的方式,SUV 组就不用每次重选了。
|
||||
- 其他车型不用改:存量值都已经是字典 key,编辑态能正常回显和保存。
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8225"
|
||||
title: "订单调整统一提交:远程调用移出锁与事务,新增失败码 587043「提交期间订单已被其他操作修改,请刷新后重试」"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "PR #8255 已合并 dev-v3(c2b261cd2),2026-09-23 15:40 滚动部署 TEST 双实例;其后另一会话部署的 f6d21648b 包含本单。真实网关实测:非法座位数提交返回 582024 且零写入,日志确认拒绝发生在拿锁之前;改出行人姓名正向往返 200 并落调整记录,已改回原值;无 token / 伪造 token 均 401。入参、出参结构不变,唯一的契约变化是新增失败码 587043;前端按通用失败提示展示后端 message 即可,无需改动。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单调整: 统一提交的远程调用移出锁与事务 + 新增失败码 587043
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8255
|
||||
> **Issue**: #8225
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台订单详情「调整订单」弹窗的统一提交
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:统一提交接口多了一个失败码 `587043「提交期间订单已被其他操作修改,请刷新后重试」`。
|
||||
- 前端以前以为的:同一订单两次调整并发提交时,后提交的那次会排队等前一次完成,然后照常成功。
|
||||
- 实际新行为:算价、协议价、车型校验这些远程调用现在在**拿锁之前**完成。如果在这段时间里另一笔调整改了本单的出发日、人数或产品,本次提交会被拒绝并返回 587043,整笔不写库;用户刷新后重新提交即可。请求和响应的结构都没有变。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 调整订单统一提交 | POST | `/v3/admin/order/:id/adjustment/submit` | 新增失败码 | 新增 587043;入参、出参不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 调整订单统一提交 `POST /v3/admin/order/:id/adjustment/submit`
|
||||
|
||||
**VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台订单详情页「调整订单」弹窗,前端在内存里收集出行人、改期、行程、用房需求、行程用车、接送机用车等改动后一次性提交。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| id | Path | Long | ✅ | 订单须存在(581007) | 订单 ID |
|
||||
| updates | Body | Object | ✅ | 至少一个子领域有实际改动(587033) | 各子领域修改内容,未改的子领域传 null |
|
||||
| updates.people | Body | Object | ❌ | - | 订单级紧急联系人 + 出行人增删改 |
|
||||
| updates.travelers | Body | Object | ❌ | - | 出行人 add / update / remove |
|
||||
| updates.schedule | Body | Object | ❌ | departDate 为 `yyyy-MM-dd` | 改期 |
|
||||
| updates.itinerary | Body | Object | ❌ | - | 行程天与节点的完整新版本 |
|
||||
| updates.hotelRequirement | Body | Object | ❌ | - | 用房需求完整新版本 |
|
||||
| updates.vehicleRequirement | Body | Object | ❌ | fleet 车型须为车型大类、座位数须在车型库选项内 | 行程用车需求完整新版本 |
|
||||
| updates.transferRequirement | Body | Object | ❌ | 同上 | 接送机用车需求完整新版本 |
|
||||
|
||||
#### 出参 `Result<AdjustmentSubmitRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 提交成功恒为 true |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"travelers": {
|
||||
"update": [{ "id": "2098374040010772481", "name": "杨知川" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "success": true }, "traceId": null, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口没有空数据场景。所有提交都没有实际改动时返回 587033,零写入:
|
||||
|
||||
```json
|
||||
{ "code": 587033, "message": "未检测到有效变更,无需提交", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本次新增:
|
||||
|
||||
```json
|
||||
{ "code": 587043, "message": "提交期间订单已被其他操作修改,请刷新后重试", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
既有错误码照旧,例如车型座位数不合法:
|
||||
|
||||
```json
|
||||
{ "code": 582024, "message": "座位数不在该车型大类可选座位数中,请检查车型库", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:未登录 → 业务码 `401`(「缺少有效的 Authorization 头」),token 签名不对 → `401`(「Token 无效」)。
|
||||
- 587043 只在「本次提交读到的订单」与「拿锁后的订单」在出发日、人数、产品上不一致时出现,也就是有另一笔调整恰好在同一时刻提交;单人操作不会遇到。
|
||||
- 587043 与其它失败码一样整笔零写入,重新提交会按最新的订单状态重新计算差价。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 只改出行人姓名 | `updates.travelers.update=[{id,name}]` | 200 |
|
||||
| ❌ 没有任何实际改动 | `updates: {}` | 587033 |
|
||||
| ❌ 行程用车座位数不在车型库选项内 | `fleet=[{vehicleType:"大巴", seats:999, count:1}]` | 582024,零写入 |
|
||||
| ❌ 提交期间另一笔调整改了出发日 / 人数 / 产品 | 任意 | 587043,零写入(**本次新增**) |
|
||||
|
||||
- 请求与响应结构没有任何变化;587043 的 `message` 已写明处理方式,前端按通用失败提示展示后端文案即可。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无 Flyway migration、无 DDL、无表结构变更。
|
||||
- 写库集合与改前一致(出行人、行程、用房/用车需求版本、优惠加费、调整记录、状态日志),仍在同一个事务内原子完成。
|
||||
- 变化在事务边界:算价、协议价、车型校验等远程调用改为在拿锁、开事务之前完成,订单行与用车确认 fence 行的锁持有时间不再包含这些远程往返。
|
||||
- 任何失败(含新增的 587043)都在写库之前抛出,整笔零写入。TEST 实测:非法座位数提交后,订单主单、fence 行、用车需求、调整记录前后逐字段一致。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 业务码 `401`。
|
||||
- 越权规则不变:非管理员只能调整自己负责的订单。
|
||||
- 拒绝原因的先后顺序不变:终态、已过天锁定、出行人/改期窗口、改期日期格式等守卫,仍然先于算价与车型校验。
|
||||
- 团期子订单改期仍只能改回所属团期出发日(587039),不走个人价格日历报价。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 请求体 | `AdjustmentSubmitReqVO` | 不变 |
|
||||
| 响应体 | `{ success }` | 不变 |
|
||||
| 失败码 | 无 587043 | 新增 587043 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 远程调用(算价、协议价、车型字典/座位、字典标签)发生时机 | 拿锁、开事务之后 | 拿锁、开事务之前 |
|
||||
| 两笔调整并发提交,前一笔改了出发日/人数 | 后一笔等锁后按新状态照常执行 | 后一笔返回 587043,刷新重提 |
|
||||
| 车队 / 产品 / 资源服务响应慢 | 订单行被锁住直到远程返回 | 不影响锁持有时间 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:请求、响应结构不变;只新增一个失败码,只在并发调整同一订单时出现。
|
||||
- **前端是否必须同步上线**:否。前端的通用失败提示会展示后端 `message`(「提交期间订单已被其他操作修改,请刷新后重试」),无需改动。
|
||||
- **前端 workaround 清理点**:无。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`POST /v3/admin/order/:id/adjustment/submit` 的内部执行顺序,以及新增失败码 587043。
|
||||
- **零影响**:
|
||||
- `GET /v3/admin/order/:id/adjustment/snapshot`(调整弹窗初始化数据)与 `GET /v3/admin/order/:id/adjustment-record`(调整记录)。
|
||||
- 行程查询 `getItinerary` 返回的服务标准字段、大交通批次列表的旅客类型中文名(管理端读接口行为不变)。
|
||||
- 既有失败码与文案。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:PR #8255 合并 `dev-v3`(`c2b261cd2`),2026-09-23 15:40 滚动部署 TEST 双实例;其后另一会话部署的 `f6d21648b` 以本单为祖先,且其间没有提交改动本单涉及的文件。
|
||||
|
||||
真实网关(`https://api.test.1814.love`,管理端 token,订单 `2098372757820428289`):
|
||||
|
||||
```
|
||||
POST /v3/admin/order/:id/adjustment/submit 无 token → 401 缺少有效的 Authorization 头 ✓
|
||||
POST /v3/admin/order/:id/adjustment/submit 伪造 token → 401 Token 无效 ✓
|
||||
POST ... updates={} → 587033 未检测到有效变更 ✓
|
||||
POST ... vehicleRequirement.fleet=[大巴 × 999 座] → 582024,订单/fence/用车需求/调整记录前后一致 ✓
|
||||
同时刻 primary 实例日志:先是不加锁的 order_main 查询,全程 0 条 fence SQL、0 条 FOR UPDATE ✓
|
||||
POST ... travelers.update 改姓名 → 200,姓名已改、调整记录 0→1 ✓
|
||||
POST ... travelers.update 改回原名 → 200,姓名还原 ✓
|
||||
```
|
||||
|
||||
587043 需要两笔调整真实竞态才能触发,TEST 上未造出;由单元测试覆盖(两段之间出发日变化 → 587043 且零写入;出发日未变 → 放行的对照)。
|
||||
|
||||
单元测试:rebase 后定向 239 类 / 3290 例 0 失败,含 `AdjustmentServiceSubmitTest` 86、`TransactionalRemoteCallArchTest` 2、`SettlementLockContractTest` 5。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8225](https://git.1814.love/wx/HL/issues/8225)
|
||||
- 关联 PR: [wx/HL#8255](https://git.1814.love/wx/HL/pulls/8255)
|
||||
- 上游来源:#8202(CR 带出本单)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8225](https://git.1814.love/wx/HL/issues/8225)
|
||||
- **PR**: [#8255](https://git.1814.love/wx/HL/pulls/8255)
|
||||
- **Merge commit**: [c2b261cd2](https://git.1814.love/wx/HL/commit/c2b261cd2)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,348 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8226"
|
||||
title: "车型大类中文名空白校验收紧(全角空格等 Unicode 空白改为拒绝)+ 空名大类不再被挤出车型字典白名单"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "9e0897c2934a6720fb9cbab05b1e14394c74c8f6"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "PR #8232 已合并 dev-v3(9231b8d8c),2026-09-23 14:30 滚动部署 TEST 双实例(检出 7a876ec57)。真实网关实测:新增/编辑车型大类 typeName 传全角空格、半角空格、空串、null 均返回 400「大类中文名不能为空」,库内零写入;读侧临时把 mpv 名称置空后,order-v3 车型白名单仍含 mpv(展示名回落为 mpv),验毕已原样还原。前端待办:车型大类表单的 typeName 必填校验需同样把全角空格视为空。前端已交付(2026-09-23):CategoryEditModal typeName 必填规则改 trim() 判空 validator,纯全角/半角空白提交前拦下(与后端 400 同口径),首尾带空格正常名放行且提交不 trim 原样发;新建组件 spec 4 例全绿。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车队车型管理: 车型大类中文名空白校验收紧 + 空名大类不再被挤出字典白名单
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service
|
||||
> **PR**: #8232
|
||||
> **Issue**: #8226
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台「车型管理」新增/编辑车型大类表单的 typeName 校验;order-v3 用车需求车型字典白名单(582032)的数据源
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:新增/编辑车型大类时,`typeName` 如果全部由**全角空格(U+3000)等 Unicode 空白**组成,现在返回 `400「大类中文名不能为空」`。
|
||||
- 前端以前以为的:`typeName` 只要不是空串、不全是半角空格,后端就会接受。
|
||||
- 实际新行为:以前后端用 `@NotBlank` 校验,它按 `String.trim()` 判空,只去掉半角空白,所以全角空格能写进库。可车队给 order-v3 的车型白名单按 `isBlank()` 判空名,会把这种名字当成空的、把整个大类从白名单里丢掉,结果这个大类下的用车需求被 `582032「不存在或已下线」` 误拒。现在两处同时收口:写侧拒绝这种名字;读侧即使库里出现空名,也不再把该大类挤出白名单。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 新增车型大类 | POST | `/admin/fleet/vehicle-types` | 入参校验收紧 | `typeName` 全为 Unicode 空白(含全角空格)改为 400 |
|
||||
| 2 | 编辑车型大类 | PUT | `/admin/fleet/vehicle-types/{typeId}` | 入参校验收紧 | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 新增车型大类 `POST /admin/fleet/vehicle-types`
|
||||
|
||||
**VO**: `VehicleTypeSaveReqVO → VehicleTypeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车队 → 车型管理」新增一个车型大类(如 SUV系列 / 商务车)时调用。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| typeKey | Body | String | ✅ | 非空白,≤16,全局唯一(600101) | 大类标识 key,如 `suv` |
|
||||
| typeName | Body | String | ✅ | **至少含一个非空白字符(空白按 Unicode 判定,全角空格也算空白,本次收紧)**,≤64 | 大类中文名 |
|
||||
| icon | Body | String | ❌ | ≤64 | 图标组件名或 emoji |
|
||||
| description | Body | String | ❌ | ≤256 | 大类描述 |
|
||||
| sortOrder | Body | Integer | ✅ | 非 null | 排序权重,升序 |
|
||||
|
||||
#### 出参 `Result<VehicleTypeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 大类 ID |
|
||||
| typeKey | String | 大类 key |
|
||||
| typeName | String | 大类中文名 |
|
||||
| icon | String | 图标 |
|
||||
| description | String | 描述 |
|
||||
| sortOrder | Integer | 排序权重 |
|
||||
| seatOptions | Array\<Integer\> | 座位数选项,新建恒为 `[]` |
|
||||
| modelCount | Integer | 型号数,新建恒为 0 |
|
||||
| inUseCount | Integer | 在役车辆数,新建恒为 0 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"typeKey": "mpv",
|
||||
"typeName": "商务车",
|
||||
"icon": "",
|
||||
"description": "",
|
||||
"sortOrder": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "2064609312168083458",
|
||||
"typeKey": "mpv",
|
||||
"typeName": "商务车",
|
||||
"icon": "",
|
||||
"description": "",
|
||||
"sortOrder": 2,
|
||||
"seatOptions": [],
|
||||
"modelCount": 0,
|
||||
"inUseCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口没有空数据场景,也没有降级分支;校验不通过时零写入。
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "大类中文名不能为空", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`typeName` 为 null、空串、纯半角空格、纯全角空格,一律返回同一条错误,且只报一条:
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "大类中文名不能为空", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 600101, "message": "车型大类标识已存在", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:未登录 → 业务码 `401`(「缺少有效的 Authorization 头」)。
|
||||
- 只拒绝**全部**由空白组成的名字;首尾带空格的正常名(如 `" 商务车 "`)照常放行,后端不做 trim。
|
||||
- 以前只有全角空格这一类会被放行,现在也拒绝;null、空串、半角空格在改前就被拒,行为不变。
|
||||
|
||||
---
|
||||
|
||||
### 2. 编辑车型大类 `PUT /admin/fleet/vehicle-types/{typeId}`
|
||||
|
||||
**VO**: `VehicleTypeSaveReqVO → VehicleTypeRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车型管理」编辑已有大类的名称、图标、描述或排序。`typeKey` 不可改,后端忽略入参中的 `typeKey`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| typeId | Path | Long | ✅ | 大类须存在(600102) | 大类 ID |
|
||||
| typeName | Body | String | ✅ | **至少含一个非空白字符(空白按 Unicode 判定,全角空格也算空白,本次收紧)**,≤64 | 大类中文名 |
|
||||
| icon | Body | String | ❌ | ≤64;不传则不改 | 图标 |
|
||||
| description | Body | String | ❌ | ≤256;不传则不改 | 描述 |
|
||||
| sortOrder | Body | Integer | ✅ | 非 null | 排序权重 |
|
||||
|
||||
#### 出参 `Result<VehicleTypeRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 大类 ID |
|
||||
| typeKey | String | 大类 key(不可改) |
|
||||
| typeName | String | 大类中文名 |
|
||||
| icon | String | 图标 |
|
||||
| description | String | 描述 |
|
||||
| sortOrder | Integer | 排序权重 |
|
||||
| seatOptions | Array\<Integer\> | 该大类下型号座位数选项,去重升序 |
|
||||
| modelCount | Integer | 型号数 |
|
||||
| inUseCount | Integer | 在役车辆数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"typeName": "商务车",
|
||||
"icon": "",
|
||||
"description": "",
|
||||
"sortOrder": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "2064609312168083458",
|
||||
"typeKey": "mpv",
|
||||
"typeName": "商务车",
|
||||
"sortOrder": 2,
|
||||
"seatOptions": [7],
|
||||
"modelCount": 1,
|
||||
"inUseCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口没有空数据场景,也没有降级分支;校验不通过时零写入,`update_time` 也不变。
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "大类中文名不能为空", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "大类中文名不能为空", "data": null, "traceId": null, "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 600102, "message": "车型大类不存在", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权:未登录 → 业务码 `401`。
|
||||
- 参数校验先于「大类是否存在」:`typeName` 为空白时,即使 `typeId` 不存在也返回 400,而不是 600102。
|
||||
- 编辑时把名字清空是被拒绝的操作,不会把一个在营大类变成空名大类。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 正常中文名 | `typeName: "商务车"` | 通过 |
|
||||
| ✅ 首尾带空格 | `typeName: " 商务车 "` | 通过,原样落库 |
|
||||
| ❌ 纯全角空格 | `typeName: " "` | 400(**本次新增拒绝**) |
|
||||
| ❌ 纯半角空格 / 空串 / null | `typeName: " "` / `""` / 不传 | 400(改前已拒) |
|
||||
|
||||
- 前端表单的必填校验应与后端一致,按「去掉**全部 Unicode 空白**后是否为空」判定。JS 的 `String.prototype.trim()` 会去掉全角空格(U+3000),可以直接用 `value.trim() === ''` 判空。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无 Flyway migration、无 DDL、无表结构变更。`fleet_vehicle_type.type_name` 原本就是 `VARCHAR(64) NOT NULL`。
|
||||
- 校验失败的请求零写入:TEST 实测 4 次 POST、2 次 PUT 被拒后,`fleet_vehicle_type` 全表逐字段(含 `update_time`)与请求前一致,也没有生成测试 key 的行。
|
||||
- 存量处理成本为 0:TEST 上在架大类 4 行,`type_name` 都非空白。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 业务码 `401`。
|
||||
- `typeName` 只要含一个非空白字符就放行;是否是「像中文的名字」后端不判断。
|
||||
- 读侧兜底(内部接口,前端不直接调用,写在这里方便排查 582032):如果库里仍出现空名大类(例如被直接改库),车队给 order-v3 的车型白名单(`GET /internal/fleet/vehicle-types/category-names`)**照样包含这个大类**,只是展示名回落为规范 key(如 `mpv`),同时车队服务打一条 WARN「车型大类名称为空,展示名回落到规范 key」。改前这个大类会从白名单里整个消失,导致用车需求报 `582032「车型 mpv 不在车型字典内(不存在或已下线)」`,而座位数校验照常通过。
|
||||
- 同一规范 key 下有多行时,展示名取排序最前、且名称非空白的那一行。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `typeName` 校验 | `@NotBlank`:按 `trim()` 判空,全角空格放行 | `@NotNull` + Unicode 空白判定:全角空格等一律拒 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 新增/编辑时 `typeName` 传全角空格 | 200,落库 | 400「大类中文名不能为空」,零写入 |
|
||||
| `typeName` 传 null / 空串 / 半角空格 | 400 | 400(同一条文案,只报一条) |
|
||||
| 库里存在空名在营大类时,order-v3 提交该大类的用车需求 | 582032 误拒(座位校验却通过) | 正常通过字典校验 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:只收紧了一种此前被接受的取值(`typeName` 全为全角空格等 Unicode 空白),这种名字本来就没有业务意义;请求/响应结构不变。
|
||||
- **前端是否必须同步上线**:否。前端不改的话,用户输入全角空格会在提交后收到后端 400 文案,不会写坏数据。建议前端表单必填校验同步用 `trim()` 判空,在提交前就提示。
|
||||
- **前端 workaround 清理点**:无。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:新增/编辑车型大类的 `typeName` 校验;内部接口 `category-names` 对空名大类的取舍。
|
||||
- **零影响**:
|
||||
- `typeKey`、`icon`、`description`、`sortOrder` 的校验规则。
|
||||
- `GET /admin/fleet/vehicle-types`(树)、`/page`、`/list` 的响应结构与内容。
|
||||
- 车型型号(`/admin/fleet/vehicle-types/{typeId}/models` 等)相关接口。
|
||||
- order-v3 的 582032 / 582033 错误码与文案本身;名称正常的大类在白名单里的名称与顺序不变(TEST 实测 4 个在架大类改前改后一致)。
|
||||
- 座位数选项接口 `seat-options` 的行为(本来就不看名称)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:PR #8232 合并 `dev-v3`(`9231b8d8c`),2026-09-23 14:30 滚动部署 TEST 双实例(检出 `7a876ec57`,含本单)。
|
||||
|
||||
构建身份(零写入判据):直连 8087 / 8187 两个实例,`PUT /admin/fleet/vehicle-types/999999999`(不存在的 id):`typeName` 传全角空格返回 400(旧代码会放行到 600102);同一 id 传正常名返回 600102(阴性对照)。
|
||||
|
||||
真实网关(`https://api.test.1814.love`,管理端 token):
|
||||
|
||||
```
|
||||
POST /admin/fleet/vehicle-types 无 token → 401 缺少有效的 Authorization 头 ✓
|
||||
POST /admin/fleet/vehicle-types typeName=" " → 400 大类中文名不能为空 ✓
|
||||
POST /admin/fleet/vehicle-types typeName=" " → 400 大类中文名不能为空 ✓
|
||||
POST /admin/fleet/vehicle-types typeName="" → 400 大类中文名不能为空 ✓
|
||||
POST /admin/fleet/vehicle-types typeName=null → 400 大类中文名不能为空 ✓
|
||||
PUT /admin/fleet/vehicle-types/{suv2 的 id} typeName=" " → 400 ✓
|
||||
PUT /admin/fleet/vehicle-types/{suv2 的 id} typeName="" → 400 ✓
|
||||
fleet_vehicle_type 全表前后逐字段一致(含 update_time)✓
|
||||
```
|
||||
|
||||
读侧(内部接口,两个实例):把 `mpv` 的 `type_name` 临时依次改为空串、半角空格、全角空格,`category-names` 均返回 `mpv → "mpv"`,其余 3 个大类名称与顺序不变,`seat-options?vehicleType=mpv` 仍返回 `[7]`;验毕 `type_name` 与 `update_time` 均恢复原值(逐字段一致)。
|
||||
|
||||
单元测试:`VehicleTypeServiceTest` 28、`VehicleTypeSaveReqVOValidationTest` 5、`VehicleTypeControllerTest` 17、`VehicleTypeControllerIdempotentTest` 6、`FleetRedLineArchTest` 18,共 74 例 0 失败。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8226](https://git.1814.love/wx/HL/issues/8226)
|
||||
- 关联 PR: [wx/HL#8232](https://git.1814.love/wx/HL/pulls/8232)
|
||||
- 上游来源:#8202(子订单行程用车车型大类接入车队字典校验,582032)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8226](https://git.1814.love/wx/HL/issues/8226)
|
||||
- **PR**: [#8232](https://git.1814.love/wx/HL/pulls/8232)
|
||||
- **Merge commit**: [9231b8d8c](https://git.1814.love/wx/HL/commit/9231b8d8c)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8228"
|
||||
title: "HOLD 派单降级车务站内信补齐跳转链接,指向车务看板"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修复"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "接口路径、请求参数、响应字段均未变,变的是 admin_message 表里这一类消息 link 字段的取值。事件识别特征为 event_code=FLEET_DISPATCH_CREATED、biz_type=FLEET_ASSIGNMENT_HOLD、category_code=SYSTEM,触发条件是 fleet 派单进 HOLD 且给司机的短信通道返回 SKIP_NO_TEMPLATE(HOLD 占位无行程短链 code,或短信模板未配置),这条降级分支不看该事件的 inapp_enabled 开关。user-service 用 Flyway V20260923_001 给该事件补上链接模板 /fleet/board?orderId=${orderId},fleet 发出的 HOLD 通知在能确定唯一订单时于 extras 里带 orderId。PR #8299 已合并(3227d495c),user-service 与 fleet 均已部署测试服,测试服实测该事件配置已是该模板。backend_status=deployed。gateway_status=not_required,无新增路由,消息仍走既有列表接口读取。frontend_status=not_required,前端零改动,link 走既有 resolveJumpLink 逻辑;车务看板当前不读取 orderId 这个 query,两种取值均落在看板首页;存量消息不回填,link 仍为空。;前端复核(mmg):resolveJumpLink 空值砍 query/原样跳转两态安全,fleet/board 目录 route.query 零命中,FLEET_ASSIGNMENT_HOLD 前端零分支——与后端 not_required 评估双向印证"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车务通知: HOLD 派单降级车务站内信补齐跳转链接
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-user-service(通知配置)+ hl-fleet-service(消息 extras)
|
||||
> **PR**: [#8299](https://git.1814.love:8443/wx/HL/pulls/8299)
|
||||
> **Issue**: [#8228](https://git.1814.love:8443/wx/HL/issues/8228)
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 站内信列表中 `event_code=FLEET_DISPATCH_CREATED` 且 `biz_type=FLEET_ASSIGNMENT_HOLD` 的记录的 `link` 字段取值;读取这些消息的接口本身未变
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**这一类“HOLD 派单降级车务”站内信的 `link` 字段现在有值了。** 消息的识别特征、触发条件都没变;变的只是这一类消息的跳转地址:本次上线之前发出的消息 `link` 为空,之后新产生的按下文规则带跳转链接。
|
||||
|
||||
---
|
||||
|
||||
## 一、触发条件与消息识别(未变)
|
||||
|
||||
fleet 派单进入 HOLD 状态、给司机的短信通道返回 `SKIP_NO_TEMPLATE`(HOLD 占位没有行程短链 code,或短信模板未配置)时,通知中心按既有规则降级为给车务角色(`VEHICLE_MANAGER`)的后台管理员发一条站内信。这条消息在 `admin_message` 表里的识别特征:
|
||||
|
||||
- `event_code = FLEET_DISPATCH_CREATED`
|
||||
- `biz_type = FLEET_ASSIGNMENT_HOLD`
|
||||
- `category_code = SYSTEM`
|
||||
|
||||
以上触发条件和识别特征本次**都没有变**。这条降级分支也**不看**该事件的 `inapp_enabled` 开关——测试服上这个开关当前是 0,降级站内信照样会发,不受它影响。
|
||||
|
||||
---
|
||||
|
||||
## 二、`link` 取值规则(本次新增)
|
||||
|
||||
| 场景 | `link` 取值 |
|
||||
|---|---|
|
||||
| fleet 能确定唯一订单 | `/fleet/board?orderId=<订单ID>` |
|
||||
| fleet 确定不了唯一订单(同一派车组的行跨了两张订单,或行上缺订单 ID) | `/fleet/board?orderId=`(query 值为空,不会报错) |
|
||||
|
||||
实现方式:user-service 用 Flyway `V20260923_001` 给该事件的通知配置补上链接模板 `/fleet/board?orderId=${orderId}`;fleet 发出的 HOLD 通知在能确定唯一订单时于 extras 里带上 `orderId`,通知中心降级为站内信时用它渲染出最终链接。
|
||||
|
||||
---
|
||||
|
||||
## 三、边界
|
||||
|
||||
1. **存量消息不回填**:本次只改配置和消息生成逻辑,不动已经发出的历史消息。测试服上 2026-08-05 至 2026-08-08 期间发出的 234 条这类站内信 `link` 仍为空,前端 `resolveJumpLink` 对空 link 返回 `null`,这些消息依旧没有跳转入口;只有之后新产生的才带链接。
|
||||
2. **车务看板当前不读取 `orderId` 这个 query**:`hl-ui`(`origin/v2.1` @ `31a198613`)的车务看板 `src/views/fleet/board/**` 目前不读取 `route.query`(对该目录 86 个文件检索 `route.query` / `useRoute` / `$route` 均为 0 命中;同一检索在兄弟目录 `src/views/fleet/matrix/` 有命中,检索本身有效)。带 `orderId` 与不带 `orderId` 的两种链接点开后都落在看板首页,不会按订单定位。
|
||||
3. **`link` 走既有跳转逻辑,前端无需新增处理**:`link` 是后台相对路由,前端现有的 `resolveJumpLink`(`src/views/notification/MyMessages/jumpBiz.js:22-32`)直接把它交给 `router.push`;`kv.endsWith('=')` 时会把整段 query 砍掉(`:27-31`),所以 `/fleet/board?orderId=` 实际会跳到 `/fleet/board`,`/fleet/board?orderId=123` 原样跳转,两种情形都不会报错。
|
||||
4. **生产环境没有这类消息**:二期(order-v3、fleet)尚未上生产,本次改动目前只在测试服可见。
|
||||
|
||||
---
|
||||
|
||||
## 四、测试环境已验证
|
||||
|
||||
- PR [#8299](https://git.1814.love:8443/wx/HL/pulls/8299),合并提交 [`3227d495c`](https://git.1814.love:8443/wx/HL/commit/3227d495c),user-service、fleet 均已部署到测试服。
|
||||
- 测试服上 `notification_event_config` 中 `event_code=FLEET_DISPATCH_CREATED` 那一行的 `inapp_link_template` 实测已是 `/fleet/board?orderId=${orderId}`。
|
||||
|
||||
---
|
||||
|
||||
## 五、可选增强(非必须,供参考)
|
||||
|
||||
如果车务看板以后想按订单定位,query 参数名已经固定是 `orderId`,值是订单 ID 字符串,直接读取即可,不需要另外跟后端约定参数名。这是可选增强,不是必须动作。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8228](https://git.1814.love:8443/wx/HL/issues/8228)
|
||||
- **PR**: [#8299](https://git.1814.love:8443/wx/HL/pulls/8299)
|
||||
- **Merge commit**: [`3227d495c`](https://git.1814.love:8443/wx/HL/commit/3227d495c)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,507 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8230"
|
||||
title: "团期订单支持用餐:生成、保存、重置、查询、套用模版可只传团期ID"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "04e08d1a9ddb8ee26f5313fb7a08e3c020225475"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "已部署 TEST 并经 Gateway 实测通过。用餐的查询、生成、保存、重置、套用模版 5 个接口加入团期维度:orderId 与 groupBatchId 必须且只能传一个,都传或都不传返回 400。只传 groupBatchId 时读写的是团期自己的用餐行(行上团期ID有值、订单ID为空);按订单读写的行此后团期ID为空,按团期查不到子订单的行。团期的出发/返回日期取团期出团日期 depart_date / 结束日期 end_date。前端结论(2026-09-23):现有子订单用餐页(MealTab)5 个调用点全只传 orderId,查询恒有 orderId 空值守卫,零 batchNo/groupBatchId 读写、零 589601 分支——「挂团订单保存勿再带 groupBatchId」「只传 batchNo 400」等破坏点前端零命中,基本行为零改动。团期维度用餐属新增能力,前端无团期用餐页面,建页为新产品需求待拍板,非本次同步范围。;前端纠正(2026-09-24):09-23 误把团期维度建页判出范围,用户指出后立项——团期详情新增「用餐」Tab(行程后,show:lazy),复用 MealTab 新增 groupBatchId prop 二选一 scope;mealInfo API 4 函数改 scope 对象+normalizeMealScope 前端断言;破坏点维持零命中;spec MealTab 30+mealInfo 5+batch 357+detail 389 全绿,checkpoint 13 项过"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期订单支持用餐,5 个用餐接口可只传团期ID
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8251
|
||||
> **Issue**: #8230
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台用餐信息的查询、生成、保存、重置与套用模版(订单、团期两种维度)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 🔁 **二选一(破坏性)**:查询、生成、保存、重置、套用模版这 5 个接口,`orderId` 与 `groupBatchId` **必须且只能传一个**。都传返回 `400`「订单ID和团期ID只能传一个」;都不传返回 `400`「订单ID和团期ID必须传一个」。查询接口的 `batchNo` 不算数,**只传 `batchNo` 视为两个都没传**,同样 400。
|
||||
- 🔁 **订单行不再带团期**:按订单生成、保存、重置、套用出来的行,行上 `groupBatchId`、`batchNo` 一律为空。以前挂团订单的行会顺带写团期ID / 团期编号,现在不写。
|
||||
- 🔁 **保存不再用 `orderId` + `groupBatchId` 一起传**:以前保存时可以同时传两者并校验订单是否属于该团期(589601),现在同时传直接 400,589601 不再出现。
|
||||
- 🆕 **团期维度**:只传 `groupBatchId` 时读写的是**团期自己的用餐行**,行上 `groupBatchId` 有值、`orderId` 为空;按团期查询**只返回团期自己的行,不含团里子订单的行**。
|
||||
- ℹ️ **3.1 用 `orderId` + `batchNo` 组合此后查不到数据**:订单行上已不存团期编号,这个组合固定返回空列表。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 查询用餐信息列表(3.1) | GET | `/v3/admin/order/meal-info/list` | 修改 | `orderId` / `groupBatchId` 二选一;只传团期ID只返回团期自己的行,顶层汇总按团期填 |
|
||||
| 2 | 生成用餐信息(3.2) | POST | `/v3/admin/order/meal-info/generate` | 修改 | 新增 `groupBatchId`,二选一;团期按团期产品当前行程生成 |
|
||||
| 3 | 保存用餐信息(整单,3.3) | POST | `/v3/admin/order/meal-info/save` | 修改 | `orderId` 不再必填,二选一;只传团期ID保存团期行 |
|
||||
| 4 | 重置用餐信息(3.4) | POST | `/v3/admin/order/meal-info/reset` | 修改 | 新增 `groupBatchId`,二选一;团期删掉自己的行后重新生成 |
|
||||
| 5 | 套用用餐模版(4.3) | POST | `/v3/admin/order/meal-template/apply` | 修改 | 新增 `groupBatchId`,二选一;同一个模版可套到团期 |
|
||||
|
||||
出参字段一个都没增减。模版查询、存为模版、删除模版不变(模版不分订单、团期)。没有新增错误码。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询用餐信息列表 `GET /v3/admin/order/meal-info/list`
|
||||
|
||||
**VO**: `OrderMealInfoListReqVO` → `OrderMealInfoListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
按订单或按团期取用餐行及顶层汇总。两种维度用同一个接口,由传 `orderId` 还是 `groupBatchId` 区分。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Query | String | 二选一 | 正数 | 按订单查。**与 `groupBatchId` 必须且只能传一个** |
|
||||
| groupBatchId | Query | String | 二选一 | 正数 | 按团期查,只返回团期自己的行 |
|
||||
| batchNo | Query | String | 否 | — | 不参与二选一;只传它按都没传处理(400);与 `orderId` 组合固定查不到数据 |
|
||||
| 其余筛选 | Query | — | 否 | — | `mealType`、`mealDateStart/End`、`dayNumber`、`settleType`、`keyword`、`limit` 不变 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | String | 按订单查为订单ID;按团期查为 `null` |
|
||||
| groupBatchId / batchNo | String | 按订单查:**仍回显订单所属团期**(不挂团为 `null`);按团期查:该团期 |
|
||||
| departDate / returnDate | String | 按订单查:订单出发/返回日期;按团期查:团期出团日期 `depart_date` / 结束日期 `end_date` |
|
||||
| personCount | Integer | 按订单查:订单人数;按团期查:团期报名人数 |
|
||||
| orderEditable | Boolean | 按团期查恒为 `true` |
|
||||
| generated | Boolean | 按团期查:该团期有没有自己的行 |
|
||||
| items[].orderId / items[].groupBatchId | String | 订单行:`orderId` 有值、`groupBatchId` 为 `null`;团期行:`groupBatchId` 有值、`orderId` 为 `null` |
|
||||
| items[].batchNo | String | 新写入的行一律为 `null` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/meal-info/list?groupBatchId=2101996448340238338
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"orderId": null,
|
||||
"groupBatchId": "2101996448340238338",
|
||||
"batchNo": "Q202611042099750379129368577",
|
||||
"departDate": "2026-11-04",
|
||||
"returnDate": "2026-11-08",
|
||||
"personCount": 4,
|
||||
"orderEditable": true,
|
||||
"generated": true,
|
||||
"totalAmount": 0.00,
|
||||
"items": [
|
||||
{ "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "batchNo": null,
|
||||
"mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
按订单查(订单挂在该团期下)时顶层 `groupBatchId` 仍是 `"2101996448340238338"`,但 `items[].groupBatchId` 为 `null`。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 团期还没有自己的行:`items: []`、`generated: false`,顶层汇总照常填。
|
||||
- 团期里子订单有用餐行、团期自己没有:按团期查仍是 `items: []`。
|
||||
- `orderId` + `batchNo`:`items: []`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null }
|
||||
```
|
||||
|
||||
都不传(含只传 `batchNo`):`400`「订单ID和团期ID必须传一个」。按团期查没有团期查看权限:`589507`。订单维度的错误码不变。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 二选一在入参校验阶段判断,先于任何权限、存在性判断。
|
||||
- 按团期查只按团期ID取行,不含团里子订单的行;按订单查只返回该订单的行。
|
||||
- 顶层 `groupBatchId` 是订单汇总回显,不代表行上存了团期ID。
|
||||
|
||||
### 2. 生成用餐信息 `POST /v3/admin/order/meal-info/generate`
|
||||
|
||||
**VO**: `OrderMealInfoGenerateReqVO` → `OrderMealInfoListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单或团期还没有用餐行时,按产品行程生成空行。团期传 `groupBatchId`。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | String | 二选一 | 正数 | 按订单产品快照生成(不变) |
|
||||
| groupBatchId | Body | String | 二选一 | 正数 | **新增**。按团期产品当前行程生成团期行 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 全部字段 | — | 与查询接口同维度的返回一致(团期维度见接口 1) |
|
||||
| items[].dayNumber | Integer | 团期:第 d 天用餐日期 = `depart_date` + d − 1,天数到 `end_date` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "groupBatchId": "2101996448340238338" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"orderId": null, "groupBatchId": "2101996448340238338",
|
||||
"departDate": "2026-11-04", "returnDate": "2026-11-08", "personCount": 4, "generated": true,
|
||||
"items": [
|
||||
{ "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 },
|
||||
{ "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "DINNER", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 团期已有自己的行:不再生成,直接返回现有行(幂等)。
|
||||
- 团期 `depart_date` 或 `end_date` 为空:不生成,`code` 200,`message` 带 589602 文案。
|
||||
- 行程里含(酒店)/ 自理的餐、行程没有的天不生成;团期产品取不到行程时生成 0 行。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "订单ID和团期ID必须传一个", "success": false, "data": null }
|
||||
```
|
||||
|
||||
都传:`400`「订单ID和团期ID只能传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`、`end_date` 早于 `depart_date` `589612`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团期生成的每天每餐取团期产品**当前**行程,不是某张子订单下单时的快照。
|
||||
- 团期不按团期状态拦截。
|
||||
- 按订单生成的行 `groupBatchId` 为空。
|
||||
|
||||
### 3. 保存用餐信息(整单) `POST /v3/admin/order/meal-info/save`
|
||||
|
||||
**VO**: `OrderMealInfoBatchSaveReqVO` → `OrderMealInfoListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
一次提交某张订单或某个团期保存后应有的全部行:带行ID覆盖、不带新增、原来有而这次没传的软删。团期**只传 `groupBatchId`**,不要同时传 `orderId`。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | String | 二选一 | 正数 | **不再必填**。按订单保存 |
|
||||
| groupBatchId | Body | String | 二选一 | 正数 | **含义变化**:以前是「订单所属团期一致校验」,现在只传它表示按团期保存 |
|
||||
| items[] | Body | Array | 是 | — | 保存后应有的全部行,字段不变;空数组表示一行都不留 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 全部字段 | — | 与查询接口同维度的返回一致 |
|
||||
| items[].dayNumber | Integer | 团期行 = 用餐日期 − `depart_date` + 1;`depart_date` 为空时为 `null` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"groupBatchId": "2101996448340238338",
|
||||
"items": [
|
||||
{ "mealType": "LUNCH", "mealDate": "2026-11-05", "restaurantName": "…", "dishName": "…",
|
||||
"unitPrice": 45.00, "tableCount": 0, "personCount": 4, "freePersonCount": 0,
|
||||
"freeAmount": 0, "otherCost": 0, "amount": 180.00, "settleType": "sign", "settleTypeName": "签单" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"orderId": null, "groupBatchId": "2101996448340238338", "departDate": "2026-11-04",
|
||||
"totalAmount": 180.00,
|
||||
"items": [
|
||||
{ "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "batchNo": null,
|
||||
"mealType": "LUNCH", "mealDate": "2026-11-05", "dayNumber": 2, "amount": 180.00 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 团期传 `items: []`:该团期自己的行全部软删,返回 `items: []`,不影响子订单的行。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null }
|
||||
```
|
||||
|
||||
都不传:`400`「订单ID和团期ID必须传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`、带的行ID不是本团期自己的行 `589611`、行查不到或已删 `589606`、金额对不上 `589613`。`589601` 不再返回。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团期保存只动团期自己的行,软删范围也只在团期自己的行里。
|
||||
- 按订单保存新增的行 `groupBatchId` 为空。
|
||||
- 任一行校验失败整批不写。
|
||||
|
||||
### 4. 重置用餐信息 `POST /v3/admin/order/meal-info/reset`
|
||||
|
||||
**VO**: `OrderMealInfoResetReqVO` → `OrderMealInfoListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
删掉某张订单或某个团期自己的全部行,再按生成规则重新生成空行。团期传 `groupBatchId`。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | String | 二选一 | 正数 | 按订单重置(不变) |
|
||||
| groupBatchId | Body | String | 二选一 | 正数 | **新增**。按团期重置 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 全部字段 | — | 与接口 2 生成的返回一致 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "groupBatchId": "2101996448340238338" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"orderId": null, "groupBatchId": "2101996448340238338", "generated": true,
|
||||
"items": [
|
||||
{ "mealInfoId": "…(新行ID)", "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 团期 `depart_date` 或 `end_date` 为空:原有团期行照样删掉、不生成,`code` 200,`message` 带 589602 文案。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589612, "message": "返回日期早于出发日期,无法生成用餐信息", "success": false, "data": null }
|
||||
```
|
||||
|
||||
`end_date` 早于 `depart_date` 时原有行不删。二选一 400、`589507`、`589500` 同接口 2。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团期重置只删团期自己的行,子订单的行不动;删和生成一起成功或一起失败。
|
||||
- 重置后行ID全部换新。
|
||||
|
||||
### 5. 套用用餐模版 `POST /v3/admin/order/meal-template/apply`
|
||||
|
||||
**VO**: `MealTemplateApplyReqVO` → `OrderMealInfoListRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
把一个模版按出发日铺开,接在现有行后面返回(只读,不写库),确认后再用接口 3 保存。模版不分订单、团期,同一个模版两边都能套。套到团期传 `groupBatchId`。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | String | 二选一 | 正数 | **不再必填**。套到订单 |
|
||||
| groupBatchId | Body | String | 二选一 | 正数 | **新增**。套到团期 |
|
||||
| templateId | Body | String | 是 | — | 模版ID(不变) |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| items[] | Array | 前段是现有行(团期:团期自己的行),后段是模版行(`mealInfoId` 为 `null`) |
|
||||
| items[].mealDate | String | 团期:模版第 d 日 = `depart_date` + d − 1 |
|
||||
| items[].personCount | Integer | 团期按人的模版行 = 团期报名人数;按桌为 0 |
|
||||
| items[].orderId / groupBatchId | String | 套到团期:模版行 `groupBatchId` 有值、`orderId` 为 `null`;套到订单:模版行 `groupBatchId` 为 `null` |
|
||||
| 其余字段 | — | 与 #8125 相同 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "groupBatchId": "2101996448340238338", "templateId": "2102035945748684802" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"orderId": null, "groupBatchId": "2101996448340238338", "departDate": "2026-11-04", "personCount": 4,
|
||||
"items": [
|
||||
{ "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-05", "dayNumber": 2 },
|
||||
{ "mealInfoId": null, "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "DINNER", "mealDate": "2026-11-06", "dayNumber": 3, "personCount": 4, "priceUnit": "person" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 团期 `depart_date` 为空:只返回团期现有行,`code` 200,`message` 带 589602 文案。
|
||||
- 模版没有行:只返回现有行。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null }
|
||||
```
|
||||
|
||||
都不传:`400`「订单ID和团期ID必须传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 套用不写库;保存时把返回的 `items` 原样用接口 3 提交,**套到团期的就只传 `groupBatchId` 提交**。
|
||||
- 团期套用的现有行只取团期自己的行,不含子订单的行。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | payload |
|
||||
|------|---------|
|
||||
| ✅ 团期生成 / 重置 | `{ "groupBatchId": "…" }` |
|
||||
| ✅ 团期保存 | `{ "groupBatchId": "…", "items": [...] }` |
|
||||
| ✅ 团期套用 | `{ "groupBatchId": "…", "templateId": "…" }` |
|
||||
| ✅ 团期查询 | `?groupBatchId=…` |
|
||||
| ✅ 订单维度 | 只传 `orderId`,其余同改动前 |
|
||||
| ❌ 两个都传 | `{ "orderId": "…", "groupBatchId": "…" }` → 400「订单ID和团期ID只能传一个」 |
|
||||
| ❌ 两个都不传 | `{}` / 只传 `batchNo` → 400「订单ID和团期ID必须传一个」 |
|
||||
|
||||
- 挂团订单保存时**不要再带 `groupBatchId`**(以前可带作一致校验,现在会 400)。
|
||||
- 团期的出发 / 返回日期、第几天都以后端返回为准(取团期 `depart_date` / `end_date`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 只传 `groupBatchId` 写入的行:团期ID有值,订单ID、团期编号为空。
|
||||
- 只传 `orderId` 写入的行:订单ID有值,团期ID、团期编号为空。
|
||||
- 团期保存、重置的软删范围只在团期自己的行;子订单的行不受影响。
|
||||
- 套用模版不写库。存量数据无需迁移。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 二选一 400 先于权限、存在性判断,库里零写入。
|
||||
- 团期写(生成 / 保存 / 重置 / 套用)要团期管理权限,查询要团期查看权限,不足 `589507`;团期不存在 `589500`;不按团期状态拦截。
|
||||
- 团期 `depart_date` / `end_date` 为空:生成、重置、套用返回 `code` 200,`message` 为「订单出发或返回日期为空,无法生成用餐信息」(沿用 589602 文案),查询照常。
|
||||
- `orderId` + `batchNo` 查询:固定空列表。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改动前 | 改动后 |
|
||||
|---|---|---|
|
||||
| 3.2 / 3.4 / 4.3 入参 `groupBatchId` | 无 | 新增,与 `orderId` 二选一 |
|
||||
| 3.3 / 4.3 入参 `orderId` | 必填 | 与 `groupBatchId` 二选一 |
|
||||
| 3.3 入参 `groupBatchId` | 与订单所属团期一致校验(589601) | 只传它表示按团期保存;与 `orderId` 同传 400 |
|
||||
| 订单行 `items[].groupBatchId` / `batchNo` | 挂团订单有值 | `null` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改动前 | 改动后 |
|
||||
|---|---|---|
|
||||
| 3.1 只传团期ID | 连同团里子订单的行一起返回,汇总为 null | 只返回团期自己的行,汇总按团期填 |
|
||||
| 3.1 都不传 | 查没挂订单也没挂团期的行 | 400 |
|
||||
| 3.1 `orderId` + `batchNo` | 按订单 + 团期编号筛 | 空列表 |
|
||||
| 3.3 都传 | 按订单保存 | 400 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是。都传、都不传、只传 `batchNo` 由成功变为 400;订单行不再带团期ID
|
||||
- **前端是否必须同步上线**: 是。挂团订单保存不能再带 `groupBatchId`;团期用餐走只传 `groupBatchId`
|
||||
- **前端 workaround 清理点**: 挂团订单保存时附带 `groupBatchId` 的处理;依赖订单行 `groupBatchId` / `batchNo` 判断归属的处理
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 上述 5 个用餐接口
|
||||
- **零影响**: 模版查询、存为模版、删除模版;金额公式;订单维度的权限与错误码(589601 除外)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署提交 `b86e635c7`(含合并提交 `6ae15b3ed`),Deploy Panel 任务 `23cffa4a`;经 Gateway(`https://api.test.1814.love`)用真实 TEST 身份(SUPER_ADMIN)由固定脚本实测 73 项全部通过。团期 `2101996448340238338`(团期编号 `Q202611042099750379129368577`,出团 2026-11-04、结束 2026-11-08、报名 4 人),子订单 `2101996448319266818`:
|
||||
|
||||
```
|
||||
只传 groupBatchId 调 3.2 生成 → 12 行,与团期产品当前行程逐天逐餐一致;第 1 天(自理/含营地/含特色餐)只有午、晚 ✓
|
||||
生成的行 → groupBatchId=团期、orderId=null、batchNo=null;mealDate=2026-11-04+d−1 ✓
|
||||
再调 3.2 → 行数、行ID不变(幂等)✓
|
||||
改一行后调 3.4 重置 → 原行全部软删,重新生成与首次逐行一致 ✓
|
||||
同一个模版只传 groupBatchId / 只传 orderId 调 4.3 → 两边都能套;团期模版行第 d 日落 2026-11-04+d−1、按人行人数=4、orderId=null ✓
|
||||
只传 groupBatchId 调 3.3 保存 2 行 → groupBatchId 有值、orderId=null;dayNumber:11-05→2、11-06→3 ✓
|
||||
再保存改一行、少传一行 → 改的变了(105.00),少传的软删 ✓
|
||||
子订单按订单保存 1 行后 3.1 按订单查回 → 行上 groupBatchId=null;顶层仍回显所属团期 ✓
|
||||
只传 groupBatchId 调 3.1 → 查不到子订单那行,只有团期保存的行 ✓
|
||||
3.1/3.2/3.3/3.4/4.3 都传 → 400「订单ID和团期ID只能传一个」(5/5)✓
|
||||
3.1/3.2/3.3/3.4/4.3 都不传、3.1 只传 batchNo → 400「订单ID和团期ID必须传一个」(6/6),零写入 ✓
|
||||
只传 groupBatchId 调 3.1 → departDate=2026-11-04、returnDate=2026-11-08,与团期详情一致 ✓
|
||||
团期不存在 / 带子订单行ID / 带不存在行ID / 金额不符 → 589500 / 589611 / 589606 / 589613,零写入 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8230](https://git.1814.love:8443/wx/HL/issues/8230)
|
||||
- 关联 PR: [wx/HL#8251](https://git.1814.love:8443/wx/HL/pulls/8251)
|
||||
- 前置变更: [#8125 套用模版改为在现有行后追加](21_8125_用餐第几天按订单出发日算与套用模版改为在现有行后追加-修改接口-管理后台.md)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8230](https://git.1814.love:8443/wx/HL/issues/8230)
|
||||
- **PR**: [wx/HL#8251](https://git.1814.love:8443/wx/HL/pulls/8251)
|
||||
- **Merge commit**: [6ae15b3ed](https://git.1814.love:8443/wx/HL/commit/6ae15b3edaa58cea64c07fce4056fef8a02b551c)
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: @lc
|
||||
@@ -0,0 +1,550 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8231"
|
||||
title: "配导游 / 配摄影 / 物资取消阶段门——出行前四态均可随时配置;取消成团改为放行并回收导摄配置"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "e15dd73bd36a1d27374f9206797243f8a5e4326c"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "jw 2026-09-23 裁决:配导游 / 配摄影 / 物资三项取消节点限制,只要在出行节点之前都可以随时配置。改前配导游 / 配摄影被 assertFormed 拦在招募中(589552),物资被 assertInMaterialStage 限死在 MATERIAL_PREPARING(589520)——而团期要集齐四项 ready(房 / 车 / 导 / 摄)才推得进 MATERIAL_PREPARING,实际「物资配不了」的根因多半是房车没配齐。本次三处写口统一引用新常量 PRE_DEPARTURE_CONFIGURABLE_STATUSES(RECRUITING / RESOURCE_PREPARING / MATERIAL_PREPARING / PENDING_DEPARTURE),出行后与已取消一律拒成新错误码 589598,未建团仍 589553。连带改的是取消成团:放开招募中可配之后,若导摄仍算「已派单资源」,招募中配过人的团一成团就再也取消不了成团(开工前实测:TEST 团期 2099750660965584898 在房车 ready 全 0、已确认子订单 0 的情况下,仅凭导游一项就返 589502)。故按方案 C,导摄两项移出 R10 判定,取消成团改为放行并在同事务内回收导摄配置(order_batch_staff 整期软删 + 各子订单 order_staff_assignment 中 source=GROUP_BATCH 的扇出行软删),房 / 车两项仍照旧阻断。取证阶段另修一处本单引入的回归:招募中清空导摄配置后 ready 位回不到 false(免闸豁免只对已成团成立,needs_* 招募中恒 0),已随 PR #8237 修复。TEST 实测 2026-09-23 12:15~12:25:物资三端点在四态各放行、在 REVIEWING / TRIP_FINISHED / CANCELLED 各返 589598 且零写入;配导摄在四态各落库成功、在三个出行后态返 589598 且零写入;取消成团对仅导摄 dispatched 的样本返 200 并完成回收,对 hotel_ready=1 / vehicle_ready=1 两个样本仍返 589502 且零写入;连续两次取消成团第二次返 589501、不重复回收。前端侧:配导游 / 配摄影按钮本来就不看 batchStatus,零改动;物资 tab 的操作列在 SuppliesPanel.vue 里按 MATERIAL_PREPARING 前置隐藏,后端放开后前端不改则本单无实际效果,交接件见同批 frontend 条目,故 frontend_status 记 pending。 | 2026-09-23 mmg 交付:SuppliesPanel 拆双判据四态放行三写口,配导摄零改动实证,随配套 frontend 条目同 commit"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期资源配置: 配导游 / 配摄影 / 物资放开到出行前四态(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8234、#8237
|
||||
> **Issue**: #8231
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情页的「配导游 / 配摄影」弹窗、「物资」tab、「取消成团」按钮
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
本次是**行为变更,不是纯增字段**,三条都与调用方此前的预期不同:
|
||||
|
||||
1. **可配窗口扩大**。以前「招募中配不了导摄」「物资只有到了准备物资阶段才配得了」,现在**出行前四态都能配**。
|
||||
2. **拒绝时的错误码换了**。以前物资拒是 `589520`、导摄拒是 `589552`,现在统一是新码 **`589598`**。按 589520 / 589552 做文案分支的前端要改。
|
||||
3. **取消成团的阻塞条件收窄了**。以前「已派导游 / 已派摄影」会把取消成团拦住(589502),**现在不会**;作为代价,取消成团成功后会把该团期的导摄配置一并回收,配人列表会变空。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`GroupBatchStatus` 状态机里,「出行之前」= 前四态:
|
||||
|
||||
| 允许配置 | 拒绝 |
|
||||
|---|---|
|
||||
| `RECRUITING` 招募中 · `RESOURCE_PREPARING` 资源准备中 · `MATERIAL_PREPARING` 物料准备中 · `PENDING_DEPARTURE` 待出发 | `TRAVELLING` 出行中 · `TRIP_FINISHED` 出行完毕 · `REVIEWING` 核单中 · `SETTLED` 已结算 · `CANCELLED` 已取消 |
|
||||
|
||||
`CANCELLED` 按拒绝处理:它是终态取消,不属于「出行前」语义。
|
||||
|
||||
**物资的实际约束比表面更紧**:团期要先走到 `MATERIAL_PREPARING` 才开窗,而
|
||||
`RESOURCE_PREPARING → MATERIAL_PREPARING` 的推进条件是四项 ready 齐(房 / 车 / 导 / 摄)+ 合同 / 保险。
|
||||
即今天「物资配不了」的根因多数是房车没配齐。本次放开后,物资配置不再依赖这条链。
|
||||
|
||||
**状态机推进逻辑本身不动**:`MATERIAL_PREPARING` 这个阶段、以及「四项 ready 齐才推进」那条链一律不变。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改接口 | 阶段门从「已成团」放宽到出行前四态;拒绝码 589552 → 589598 |
|
||||
| 2 | 设置报账人等级 | PUT | `/v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank` | 修改接口 | 同上,与保存口同窗口 |
|
||||
| 3 | 新增团期备品行 | POST | `/v3/admin/order/group-batch/{groupBatchId}/supplies` | 修改接口 | 阶段门从 `MATERIAL_PREPARING` 放宽到出行前四态;拒绝码 589520 → 589598 |
|
||||
| 4 | 调整团期备品数量 | PUT | `/v3/admin/order/group-batch/supplies/{batchSuppliesId}/quantity` | 修改接口 | 同上 |
|
||||
| 5 | 软删团期备品行 | DELETE | `/v3/admin/order/group-batch/supplies/{batchSuppliesId}` | 修改接口 | 同上 |
|
||||
| 6 | 取消成团 | POST | `/v3/admin/order/group-batch/{groupBatchId}/cancel-group` | 修改接口 | 导摄两项不再阻断;成功时额外回收导摄配置 |
|
||||
|
||||
路径、入参结构、权限码、成功响应结构均零变化;网关无改动。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期人员配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO` → `BatchStaffConfigRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「配导游 / 配摄影」弹窗点保存。整期全量覆盖:按 `scopeRoles`(不传即全量)软删旧配置、
|
||||
写入新配置、异步扇出到各活跃子订单,并按配置结果回填 `guide_ready` / `photographer_ready`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 正整数,产品侧排期 ID | 团期所属班期;未建团返 589553 |
|
||||
| scopeRoles | body | List<String> | 否 | 元素非空 | 限定本次覆盖的角色范围,不传则整期覆盖 |
|
||||
| staffList | body | List<Item> | 否 | null 按空列表处理 | 传空列表 = 清空覆盖范围内的配置 |
|
||||
| staffList[].staffId | body | Long | 是 | 须命中候选人员 | 人员 ID |
|
||||
| staffList[].staffRole | body | String | 是 | 须与人员类型相符,否则 582114 | 角色(GUIDE / LEADER / PHOTOGRAPHER …) |
|
||||
| staffList[].sortOrder | body | Integer | 否 | — | 展示排序 |
|
||||
| staffList[].remark | body | String | 否 | — | 备注 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| productBatchId | Long | 回显路径参数 |
|
||||
| groupBatchId | Long | 团期聚合主键,取守卫阶段那一次既有反查的结果,不额外查库 |
|
||||
| staffList | List | 保存后的整期最终状态 |
|
||||
| affectedOrderCount | Integer | 本次扇出触及的活跃子订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/group-batch/2097250420299530242/staff HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"staffList":[{"staffId":1002,"staffRole":"GUIDE","sortOrder":0},{"staffId":1003,"staffRole":"PHOTOGRAPHER","sortOrder":1}]}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"productBatchId": "2097250420299530242",
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"affectedOrderCount": 0,
|
||||
"staffList": [
|
||||
{"staffId": 1002, "staffRole": "GUIDE", "staffName": "李雪梅", "reporterRank": "NONE"},
|
||||
{"staffId": 1003, "staffRole": "PHOTOGRAPHER", "staffName": "王强", "reporterRank": "NONE"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
`staffList` 传空列表即清空覆盖范围内的配置,返回 200,`staffList` 为空数组、`affectedOrderCount` 为实际扇出订单数。
|
||||
**清空后 ready 位的回落口径本次有修正**:已成团且该位 `needs_*=false`(成团免闸置位)时保持就绪、不回落;
|
||||
其余情形(含**招募中**)一律回落为 false。改前招募中清空后 ready 位会卡在 true,与配置行数 0 自相矛盾。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589598, "message": "出行后不可再配置导游 / 摄影 / 物资", "data": null}
|
||||
```
|
||||
|
||||
未建团(该班期还没有任何订单,团期行尚未 Lazy 建)仍是另一个码,两者不可合并:
|
||||
|
||||
```json
|
||||
{"code": 589553, "message": "团期尚未创建(该班期还没有任何订单),请先建团并完成成团后再操作", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 出行前四态(`RECRUITING` / `RESOURCE_PREPARING` / `MATERIAL_PREPARING` / `PENDING_DEPARTURE`)放行
|
||||
- `TRAVELLING` / `TRIP_FINISHED` / `REVIEWING` / `SETTLED` / `CANCELLED` 返 589598,**被拒时一行都不写**(闸在写库之前)
|
||||
- 未建团返 589553,且**不会为配置动作提前建团期行**
|
||||
- 589552「团期尚未成团」不再由本接口抛出,但该码仍被别处(车辆派单、房务)使用,未退役
|
||||
|
||||
---
|
||||
|
||||
### 2. 设置报账人等级 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`
|
||||
|
||||
**VO**: `ReporterRank`(枚举入参)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期人员名单里把某个已配人员标为主报账人 / 协助报账人。它与保存口是同构写口——同样写
|
||||
`order_batch_staff` 并扇出 `order_staff_assignment`,故口径必须与保存口完全一致。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 正整数 | 产品侧排期 ID |
|
||||
| staffId | path | Long | 是 | 须命中该团期已配人员 | 人员 ID |
|
||||
| rank | body | String | 是 | PRIMARY / ASSIST / NONE | 报账人等级 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | Void | 成功返回 null,判成败看 `code` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/group-batch/2097250420299530242/staff/1002/reporter-rank HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"rank":"PRIMARY"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code": 200, "message": "成功", "data": null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口无列表出参,无空数据形态;团期已配人员为空时会先因 staffId 未命中报错,不会静默成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589598, "message": "出行后不可再配置导游 / 摄影 / 物资", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 窗口与保存口完全一致,不允许两个方法口径漂移
|
||||
- 被拒时连「查已派 staff」这一步读都不发生,零写入
|
||||
- 未建团仍返 589553
|
||||
|
||||
---
|
||||
|
||||
### 3. 新增团期备品行 `POST /v3/admin/order/group-batch/{groupBatchId}/supplies`
|
||||
|
||||
**VO**: `AddSuppliesReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「物资」tab 手工录入一行备品(产品侧没有、临时采购的)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchId | path | Long | 是 | 正整数,团期聚合主键 | 不存在返 GROUP_BATCH_NOT_FOUND |
|
||||
| suppliesName | body | String | 否 | 传 suppliesResourceId 时可空 | 备品名称;纯手填时必填 |
|
||||
| suppliesResourceId | body | Long | 否 | — | 备品库资源 ID,传了则名称取库值 |
|
||||
| category | body | String | 否 | — | 分类 |
|
||||
| hasCost | body | Boolean | 否 | — | 是否计费 |
|
||||
| billingType | body | String | 否 | PER_PERSON / PER_QUANTITY | 计费方式 |
|
||||
| unitPrice | body | BigDecimal | 否 | — | 单价 |
|
||||
| quantity | body | Integer | 是 | ≥ 1,否则 589522 | 数量 |
|
||||
| sortOrder | body | Integer | 否 | 默认 0 | 展示排序 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | Long | 新建备品行 ID(`batchSuppliesId`),用于后续改数量 / 软删 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2099750365778886657/supplies HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"suppliesName":"临时采购的雨衣","quantity":1,"sortOrder":999}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code": 200, "message": "成功", "data": "2102611411027066881"}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口是写接口,无空数据形态。创单时由系统固化产品侧备品的 `freezeFromProduct` 路径**不过本阶段门**——
|
||||
它是首单懒建事务内的系统行为,加门会把建团直接打断,该豁免本次不变。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589598, "message": "出行后不可再配置导游 / 摄影 / 物资", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 出行前四态放行,尤其 `RESOURCE_PREPARING`——改前该态必被 589520 拒
|
||||
- 出行后与 `CANCELLED` 返 589598,不落库
|
||||
- 团期不存在返 `GROUP_BATCH_NOT_FOUND`,不是阶段错误
|
||||
- 数量非法返 589522 参数错误,不是状态错误
|
||||
|
||||
---
|
||||
|
||||
### 4. 调整团期备品数量 `PUT /v3/admin/order/group-batch/supplies/{batchSuppliesId}/quantity`
|
||||
|
||||
**VO**: `AdjustSuppliesQuantityReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
物资 tab 行内编辑数量。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| batchSuppliesId | path | Long | 是 | 须命中活跃备品行 | 不存在返 589521 |
|
||||
| quantity | body | Integer | 是 | ≥ 1,否则 589522 | 新数量 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | Void | 成功返回 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/order/group-batch/supplies/2102611411027066881/quantity HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"quantity":5}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code": 200, "message": "成功", "data": null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参,无空数据形态。备品行已被软删时返 589521,不会静默成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589598, "message": "出行后不可再配置导游 / 摄影 / 物资", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 阶段门由该备品行反查所属团期后判定,窗口与新增口一致
|
||||
- **「确认物资之后能不能改」不归本门管**:确认后仍可增删改是有意为之,窗口扩大后该结论不变
|
||||
- 被拒时数量不变、行不被软删
|
||||
|
||||
---
|
||||
|
||||
### 5. 软删团期备品行 `DELETE /v3/admin/order/group-batch/supplies/{batchSuppliesId}`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
物资 tab 删除一行备品。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| batchSuppliesId | path | Long | 是 | 须命中活跃备品行 | 不存在返 589521 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | Void | 成功返回 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/group-batch/supplies/2102611411027066881 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code": 200, "message": "成功", "data": null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
软删是幂等的:对已软删的行再调返 589521「备品行不存在」,不会重复写。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589598, "message": "出行后不可再配置导游 / 摄影 / 物资", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 窗口与新增 / 改数量完全一致
|
||||
- 被拒时行的 `deleted_at` 保持为空
|
||||
|
||||
---
|
||||
|
||||
### 6. 取消成团 `POST /v3/admin/order/group-batch/{groupBatchId}/cancel-group`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期从「资源准备中」退回「招募中」。本次**没有改它的入参或路径**,改的是它的阻塞条件与成功时的副作用。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchId | path | Long | 是 | 正整数,团期聚合主键 | 不存在返 GROUP_BATCH_NOT_FOUND;非 `RESOURCE_PREPARING` 返 589501 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| data | Void | 成功返回 null,团期状态已回到 `RECRUITING` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2099750660965584898/cancel-group HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{"code": 200, "message": "成功", "data": null}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
成功时无返回体。该团期没有任何导摄配置时,回收步骤是零行软删的空操作,不影响成功。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{"code": 589502, "message": "取消成团被阻塞(有已确认子订单或已派单资源)", "data": null}
|
||||
```
|
||||
|
||||
连续两次调用,第二次因状态已不是 `RESOURCE_PREPARING` 被 CAS 拒:
|
||||
|
||||
```json
|
||||
{"code": 589501, "message": "团期状态不允许当前操作", "data": null}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仍阻塞:有已确认子订单(`PENDING_DEPARTURE` / `TRAVELLING` / `COMPLETED`)、`hotel_ready=true`、
|
||||
`vehicle_ready=true`(且非整团免车)
|
||||
- **不再阻塞**:已派导游、已派摄影
|
||||
- 成功时依次:状态 CAS 回 `RECRUITING` → 四项 ready 与物资确认全部重置 → **回收导摄配置** → 登记配车释放命令
|
||||
- 回收范围只含 `source=GROUP_BATCH` 的扇出行,子订单自己单独派的人不受影响
|
||||
- 幂等:第二次调用被 589501 拒,不重复回收
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **判拒绝原因不要再认 589520 / 589552**。这两个码不再由上述五个配置接口抛出(589520 保留占位防号段复用,589552 仍由车辆派单、房务等别处使用)。统一认 **589598**。
|
||||
- **589598 与 589553 是两件事**,不要合并成一个 toast:前者是「这个团已经出发了,配不了了」,后者是「这个班期还没有任何订单,先去建团」。
|
||||
- 配导摄成功后,若调用方缓存了 `guide_ready` / `photographer_ready`,需重新拉取团期详情——本次修正了清空时的回落口径。
|
||||
- 取消成功后,**团期人员名单会变空**,调用方若停在配人弹窗需主动刷新。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 配导摄保存:整期(或 `scopeRoles` 范围内)软删旧配置行 + 写入新配置行 + 异步扇出到各活跃子订单;随后按配置结果回填团期行的两个 ready 标志
|
||||
- 物资三口:分别为插入一行、更新一行的数量、软删一行
|
||||
- 取消成团:状态 CAS 一行 + 重置该团期四项 ready 与物资确认 + 软删该团期全部导摄配置行 + 软删其各子订单中来源为团期扇出的人员分配行,**全部在同一个事务内**,任一步失败整体回滚
|
||||
- 被阶段门拒绝时零写入:闸在所有写库动作之前
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- `CANCELLED`(已流团)按拒绝处理,返 589598
|
||||
- 未建团(团期行尚未 Lazy 建)返 589553,**不会为配置动作提前建行**——否则会出现「零单可配 → 首单落地反而不可配 → 成团后又可配」这种非单调行为
|
||||
- 创单时固化产品侧备品的系统路径不过阶段门,该豁免不变
|
||||
- 「确认物资之后仍可增删改」不变
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 配导摄可配状态 | 已成团各态(招募中被拒) | `RECRUITING` / `RESOURCE_PREPARING` / `MATERIAL_PREPARING` / `PENDING_DEPARTURE` |
|
||||
| 配导摄拒绝码 | 589552(未成团)/ 589553(未建团) | 589598(已出行 / 已取消)/ 589553(未建团) |
|
||||
| 物资三口可配状态 | 仅 `MATERIAL_PREPARING` | 同上四态 |
|
||||
| 物资三口拒绝码 | 589520 | 589598 |
|
||||
| 取消成团:已派导游 / 摄影 | 返 589502 被阻塞 | 放行,且回收导摄配置 |
|
||||
| 取消成团:房 / 车 ready | 返 589502 被阻塞 | **不变**,仍返 589502 |
|
||||
| 招募中清空导摄后的 ready 位 | 卡在 true(与配置行数 0 矛盾) | 回落为 false |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 面 | 评估 |
|
||||
|---|---|
|
||||
| 管理后台配导摄 | 按钮本来就不看 `batchStatus`,招募中点得下去、只是被后端拒。后端放开后**前端零改动即可生效** |
|
||||
| 管理后台物资 tab | 操作列按 `MATERIAL_PREPARING` 前置隐藏,**前端不改则本次无实际效果**,交接件见同批 frontend 条目 |
|
||||
| 已有错误文案分支 | 按 589520 / 589552 做文案的地方需改认 589598 |
|
||||
| 取消成团 | 可取消的团期变多;成功后人员名单会被清空,属预期 |
|
||||
| 小程序端 | 不受影响,物资清单在小程序端是只读视图 |
|
||||
| 状态机 | 不变,`MATERIAL_PREPARING` 阶段与「四项 ready 齐才推进」那条链一律不动 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 团期状态机的推进逻辑、四项 ready 的计算口径
|
||||
- 流团(`disband`):只校验状态白名单,不看 ready 位,本次完全不受影响
|
||||
- 子订单自己单独派的人员(`order_staff_assignment` 中 source 非 `GROUP_BATCH` 的行):回收不碰它们
|
||||
- 车辆派单、房务等仍在用 `assertFormed` 的写口:口径不变
|
||||
- 权限码、路径、入参结构、成功响应结构、网关配置
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
2026-09-23 12:15~12:25,`api.test.1814.love:9443`,分支 `dev-v3`(`fe752f16c`)。
|
||||
|
||||
| AC | 内容 | 结果 |
|
||||
|---|---|---|
|
||||
| 物资四态放行 | `RECRUITING` / `RESOURCE_PREPARING` / `MATERIAL_PREPARING` / `PENDING_DEPARTURE` 各跑「增 → 改数量 → 删」三端点 | 12 次调用全 200,自造行自删、零残留 |
|
||||
| 物资出行后拒 | `REVIEWING` / `TRIP_FINISHED` / `CANCELLED` 各跑三端点 | 全返 589598;既有行的 `quantity` 未变、未软删、未新增行 |
|
||||
| 配导摄四态放行 | 四态各保存「1002 GUIDE + 1003 PHOTOGRAPHER」 | 全 200 且落库;逐个还原为改前配置,四组还原判定全 ✔ |
|
||||
| 配导摄出行后拒 | `REVIEWING` / `TRIP_FINISHED` / `CANCELLED` | 全返 589598;活跃行与表内总行数逐字未变 |
|
||||
| 未建团 | 不存在的 `productBatchId` | 返 589553,未被新码吞掉 |
|
||||
| 取消成团放行 + 回收 | 团期 `2099750660965584898`(`needs_guide=1 & guide_ready=1`,房车 ready 全 0,已确认子订单 0)——同一样本开工前实测返 589502 | 返 200;状态 `RESOURCE_PREPARING → RECRUITING`;`guide_ready` / `photographer_ready` 归 0;配置行与子订单扇出行同时间戳软删 |
|
||||
| 房车仍阻断 | `hotel_ready=1` 与 `vehicle_ready=1` 两个样本 | 均返 589502;团期行与两张配置表逐字未变 |
|
||||
| 幂等 | 对同一团期连调两次取消成团 | 第二次返 589501;三处读数与第一次后完全一致,无重复回收 |
|
||||
|
||||
本地单测:assignment + groupbatch + house 4103 条、archunit / core / requirement 等 3456 条、其余 26 个包 4621 条,全绿。
|
||||
|
||||
取证用到的写操作及还原:新增的备品行均由本次自行软删;配导摄的四个团期均按改前配置逐条还原;
|
||||
取消成团样本 `2099750660965584898` 已按改前值还原(状态、两个 ready 位、配置行与扇出行的软删标记)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8231(含开工前的 AC-6 实测与方案 C 裁决)
|
||||
- PR #8234(主体)、PR #8237(ready 回落回归修复)
|
||||
- 被推翻:#7287 T5(`assertFormed`)、#7023 AC-TD-13(物资单一阶段窗口)、wx 2026-09-15「放行但配置行保留」
|
||||
- 保持不变:#7023 AC-TD-15(确认后仍可改)、#7441 PR-2e(整团免车的 ready 剥离)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg(物资 tab 显隐条件,见同批 frontend 条目)
|
||||
@@ -0,0 +1,644 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8249"
|
||||
title: "团期「查看需求」:无需求行的户不再报 PENDING、户级未提交用车需求进预检清单、六条车务报文改写为可读名称"
|
||||
consumer: "admin"
|
||||
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: "前端判 not_required:状态列 statusName||status||— 三级兜底 null 安全;PENDING 判等 12 处全无关;vehicleMissing 按 orderNo 结构化分流即契约推荐判据;809 报文零文本匹配分支(拦截器透 toast/detail 直显)"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期「查看需求」页三处契约变更(含报文变更)
|
||||
|
||||
> **存放目录**: `changelogs-v2/{YYYY-MM}/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3 (端口 8007)
|
||||
> **PR**: #8286
|
||||
> **Issue**: #8249
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」tab 的子订单列表状态列、整团确认预检横幅、车务相关弹窗文案
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **状态字段会返 `null` 了**:`hotelRequirementStatus` / `vehicleRequirementStatus` 在「该户一条 active 需求行都没有」时返回 `null`。改前恒回落字符串 `"PENDING"`,页面把一个根本没提交的户显示成「待房务配 / 待车队配」,与同屏预检横幅里的 `NOT_SUBMITTED` 自相矛盾。配对的 `*StatusName` 此时按该户是否需要该资源分叉:需要 → `"未提交"`,不需要 → `null`。
|
||||
2. **预检清单多一类条目**:`confirm-check` 的 `vehicleMissing[]` 新增 `reason = "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED"`(错误码 809122),表示「该户在团需车、却一条行程用车需求都没提交」。它与团级的 `GROUP_REQUIREMENT_NOT_FOUND` **并列出现**,不互斥、不被团级缺席短路——改前车侧只查团级正式需求,「某户一行都没提交」在预检里没有任何位置能表达。
|
||||
3. 🔴 **报文变更**:六个 809 段错误码的**渲染文本**被改写(雪花 ID → 可读名称 / 直接删掉标识符),见「六.6、修改前后对比」。因为 `GroupVehicleViolation.detail()` 就是 `toException().getMessage()`,**预检横幅上的 `detail` 与确认/保存/免车弹窗里的 `message` 是同一份字符串**,两处同时改变。凡是对 `vehicleMissing[].detail` 或错误 `message` 做**文本匹配 / 正则解析 / 截取订单号**的前端逻辑,这次会失效;纯展示(原样渲染这段人话)不需要改。订单号已经由 `vehicleMissing[].orderNo` 结构化下发,不要再从 `detail` 里抠。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
「查看需求」页上同一个团期的同一户,三处读数互相矛盾:子订单列表说「待房务配」(=已提交、等资源侧接),预检横幅说「未提报」,而整团确认点下去报的是团级的「尚未形成正式用车需求」——运营据此去催车务,实际该催的是定制师。
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 无 active 需求行时 `*Status` | `"PENDING"`(借用了真实状态之一) | `null` |
|
||||
| 无 active 需求行时 `*StatusName` | 「待房务配」/「待车队配」 | 「未提交」(该户需要该资源时)/ `null` |
|
||||
| 车侧预检覆盖粒度 | 只有团级正式需求的六条校验 | 团级六条 + **逐户**查行程用车需求是否提交 + 待放行接送机需求服务日回填 |
|
||||
| 车务报文里的标识符 | 雪花 ID(`团期 2100856430494973953`、`子订单 2102309919002943489`) | 可读名称(`团期「第3期 10月8日出发团」`、`子订单 HL20260922161158291`)或直接省略 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | A3 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 出参取值变更 | 四个需求状态字段在无 active 行时改为 `null` + 「未提交」 |
|
||||
| 2 | GB-ADM-012 整团确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 出参枚举扩充 + 报文变更 | `vehicleMissing[].reason` 新增 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`;`detail` 文案改写 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. A3 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
|
||||
|
||||
**VO**: `Query 参数 → PageResult<GroupBatchOrderItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期详情 → 查看需求 / 子订单」tab 打开时调用,渲染该团期下每一户的一行摘要(团号、客户、金额、房需求状态、车需求状态、定制师、出行人)。本次改动只影响其中四个需求状态字段的取值,分页与其余字段的契约不变。鉴权走团期权限码 `group-batch:view`,未登录由网关拦截。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID |
|
||||
| page | Query | Integer | ❌ | 缺省 1;小于 1 归一为 1 | 页码,从 1 起 |
|
||||
| pageSize | Query | Integer | ❌ | 缺省 20;小于 1 归一为 20;大于 200 截断为 200 | 每页条数 |
|
||||
| includeTravelers | Query | Boolean | ❌ | 缺省 true | 是否附 `travelers[]`;证件号/手机号一律不返回 |
|
||||
| includeNeeds | Query | Boolean | ❌ | 缺省 true | 是否附 `roomCount` / `roomType` / `specialNeeds` |
|
||||
| includeCancelled | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单;缺省只返活跃集 |
|
||||
|
||||
#### 出参 `Result<PageResult<GroupBatchOrderItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.total | Long | 总条数 |
|
||||
| data.page | Integer | 当前页码 |
|
||||
| data.pageSize | Integer | 每页条数 |
|
||||
| data.records[].orderId | String | 子订单 ID(雪花,JSON 里是字符串) |
|
||||
| data.records[].orderNo | String | 订单编号 |
|
||||
| data.records[].teamNo | String | 团号;订金支付成功后生成,未付订金为 `null` |
|
||||
| data.records[].customerName | String | 客户姓名 |
|
||||
| data.records[].participantCount | Integer | 出行人总数 |
|
||||
| data.records[].orderStatus | String | 订单状态码 |
|
||||
| data.records[].orderStatusName | String | 订单状态中文名 |
|
||||
| data.records[].payStatus | String | 支付状态(UNPAID / DEPOSIT_PAID / FULLY_PAID) |
|
||||
| data.records[].payStatusName | String | 支付状态中文名 |
|
||||
| data.records[].contractStatus | String | 合同状态;无合同时 `null` |
|
||||
| data.records[].contractStatusName | String | 合同状态中文名;无合同时 `null` |
|
||||
| data.records[].insuranceStatus | String | 保险状态;无保险时 `null` |
|
||||
| data.records[].insuranceStatusName | String | 保险状态中文名;无保险时 `null` |
|
||||
| data.records[].paidAmount | String | 已支付金额(订金 + 尾款),两位小数字符串 |
|
||||
| data.records[].balanceAmount | String | 待支付尾款金额,两位小数字符串 |
|
||||
| data.records[].hotelRequirementStatus | String | **本次变更**:房需求状态码;无 active 房需求行时为 `null`(改前回落 `"PENDING"`) |
|
||||
| data.records[].hotelRequirementStatusName | String | **本次变更**:房需求状态中文名;`hotelRequirementStatus` 为 `null` 时该户需房则为 `"未提交"`、不需房则为 `null` |
|
||||
| data.records[].vehicleRequirementStatus | String | **本次变更**:车需求状态码;无 active 行时为 `null`(改前回落 `"PENDING"`) |
|
||||
| data.records[].vehicleRequirementStatusName | String | **本次变更**:车需求状态中文名;`vehicleRequirementStatus` 为 `null` 时该户需车则为 `"未提交"`、不需车则为 `null` |
|
||||
| data.records[].consultantName | String | 定制师姓名(创单时固化) |
|
||||
| data.records[].totalPrice | String | 本户应收,两位小数字符串;取消单为 `"0.00"` |
|
||||
| data.records[].tierCode | String | 档位码(形如 `2A1C`) |
|
||||
| data.records[].tierName | String | 档位名(形如 `2成人1儿童`) |
|
||||
| data.records[].travelerInfoComplete | Boolean | 出行人资料是否齐全 |
|
||||
| data.records[].roomCount | Integer | 房数;`includeNeeds=true` 时返回 |
|
||||
| data.records[].roomType | String | 房型文本;`includeNeeds=true` 时返回,无需求行时 `null` |
|
||||
| data.records[].roomTypeName | String | 房型中文名;字典不可达时回落原值 |
|
||||
| data.records[].specialNeeds | String | 特殊需求;缺需求行时回落订单备注 |
|
||||
| data.records[].contactPhone | String | 联系人手机号(脱敏,前 3 后 4) |
|
||||
| data.records[].groupChatUnreadCount | Integer | 「联系定制师」按钮未读角标 |
|
||||
| data.records[].travelers[].name | String | 出行人姓名 |
|
||||
| data.records[].travelers[].type | String | 出行人类型(ADULT / CHILD / YOUNG_CHILD / BABY) |
|
||||
| data.records[].travelers[].age | Integer | 出团日时的周岁;缺出生日期时 `null` |
|
||||
| data.records[].travelers[].birthdayInTrip | Boolean | 是否行程期间过生日 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2100856430494973953/orders HTTP/1.1
|
||||
Host: <网关地址>
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"orderId": "2100856430239121409",
|
||||
"orderNo": "HL20260918155619496",
|
||||
"teamNo": "26-7060",
|
||||
"customerName": "王有亿",
|
||||
"participantCount": 3,
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"orderStatusName": "定制中",
|
||||
"payStatus": "DEPOSIT_PAID",
|
||||
"payStatusName": "已付定金",
|
||||
"contractStatus": null,
|
||||
"contractStatusName": null,
|
||||
"insuranceStatus": null,
|
||||
"insuranceStatusName": null,
|
||||
"paidAmount": "1500.00",
|
||||
"balanceAmount": "9440.00",
|
||||
"hotelRequirementStatus": "PENDING_REVIEW",
|
||||
"hotelRequirementStatusName": "待审核",
|
||||
"vehicleRequirementStatus": "PENDING_REVIEW",
|
||||
"vehicleRequirementStatusName": "待提交车务",
|
||||
"consultantName": "王骁",
|
||||
"totalPrice": "10940.00",
|
||||
"tierCode": "2A1C",
|
||||
"tierName": "2成人1儿童",
|
||||
"travelerInfoComplete": true,
|
||||
"roomCount": 1,
|
||||
"roomType": "DELUXE",
|
||||
"roomTypeName": "豪华房",
|
||||
"specialNeeds": "禁烟",
|
||||
"contactPhone": "137****1407",
|
||||
"groupChatUnreadCount": 0,
|
||||
"travelers": [
|
||||
{ "name": "王有亿", "type": "ADULT", "age": 41, "birthdayInTrip": false },
|
||||
{ "name": "李美丽", "type": "ADULT", "age": 36, "birthdayInTrip": false },
|
||||
{ "name": "王小明", "type": "CHILD", "age": 8, "birthdayInTrip": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"orderId": "2102309919002943489",
|
||||
"orderNo": "HL20260922161158291",
|
||||
"teamNo": "26-2355",
|
||||
"customerName": "王二麻子",
|
||||
"participantCount": 4,
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"orderStatusName": "定制中",
|
||||
"payStatus": "DEPOSIT_PAID",
|
||||
"payStatusName": "已付定金",
|
||||
"contractStatus": null,
|
||||
"contractStatusName": null,
|
||||
"insuranceStatus": null,
|
||||
"insuranceStatusName": null,
|
||||
"paidAmount": "2000.00",
|
||||
"balanceAmount": "13920.00",
|
||||
"hotelRequirementStatus": null,
|
||||
"hotelRequirementStatusName": "未提交",
|
||||
"vehicleRequirementStatus": null,
|
||||
"vehicleRequirementStatusName": "未提交",
|
||||
"consultantName": "刘畅",
|
||||
"totalPrice": "15920.00",
|
||||
"tierCode": "4A",
|
||||
"tierName": "4成人",
|
||||
"travelerInfoComplete": false,
|
||||
"roomCount": 2,
|
||||
"roomType": null,
|
||||
"roomTypeName": null,
|
||||
"specialNeeds": "干",
|
||||
"contactPhone": "185****0000",
|
||||
"groupChatUnreadCount": 0,
|
||||
"travelers": []
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期下没有活跃子订单(或 `page` 超出范围)时返回空页,信封与分页字段结构同上,`records` 为空数组,不返 404、不 500:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
无团期权限或该团期不在本人名下:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 业务失败走 HTTP 200 + `success:false` 的信封;未登录由网关返 401,不在本接口的码集里。
|
||||
- `hotelRequirementStatus` / `vehicleRequirementStatus` 与各自的 `*StatusName` **必须成对读**:`Status` 为 `null` 且 `StatusName` 为 `"未提交"`,表示该户需要这项资源但一条需求行都没提交;两者**都**为 `null` 表示该户本来就不需要这项资源,页面渲染「—」。
|
||||
- 不要再用 `status === 'PENDING'` 判断「没提交」——`PENDING` 现在唯一含义是「已提交、等资源侧接单」。判「没提交」的唯一判据是 `status == null`。
|
||||
- `*StatusName` 在遇到枚举外的未知码时回落原 code,前端渲染保持 `statusName || status || '—'` 的三级兜底即可,不会拿到空串。
|
||||
- 本接口只读,不写库;重复调用无副作用。
|
||||
- `includeCancelled=false`(缺省)时已取消子订单不出现在结果里,`total` 同口径。
|
||||
|
||||
---
|
||||
|
||||
### 2. GB-ADM-012 整团确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
|
||||
|
||||
**VO**: `Path 参数 → GroupBatchRequirementCheckRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台在点「整体确认需求」**之前**调用,用于把按钮置灰并把缺失清单摊开给运营看。它与 `POST .../requirement/confirm` 共用同一套校验:预检里出现的每一条,都会在确认时以对应错误码抛出且**零写入**。鉴权走 `group-batch:demand:confirm`。本次改动新增了车侧的户级校验,并改写了车侧 `detail` 的文案。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body |
|
||||
|
||||
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.groupBatchId | String | 团期聚合主键 |
|
||||
| data.batchStatus | String | 团期当前状态码 |
|
||||
| data.batchStatusName | String | 团期状态中文名;`batchStatus` 为 `null` 时为 `null` |
|
||||
| data.ready | Boolean | 是否可整体确认:`missing` 空 **且** `vehicleMissing` 空 **且**阶段可确认。`missing` 空而 `ready=false` 时原因在 `vehicleMissing` |
|
||||
| data.missing[] | Array | 住宿缺失清单,按 orderId 升序,每户至多一条 |
|
||||
| data.missing[].orderId | String | 子订单 ID |
|
||||
| data.missing[].orderNo | String | 子订单号 |
|
||||
| data.missing[].customerName | String | 客户姓名 |
|
||||
| data.missing[].consultantId | String | 定制师 adminId(字符串,便于拼跳转) |
|
||||
| data.missing[].consultantName | String | 定制师姓名快照 |
|
||||
| data.missing[].reason | String | 住宿缺失原因码,见六.5 |
|
||||
| data.missing[].reasonName | String | 住宿缺失原因中文名 |
|
||||
| data.missing[].dayNumber | Integer | 第几天;无该维度时 `null` |
|
||||
| data.missing[].segmentIndex | Integer | 段序;无该维度时 `null` |
|
||||
| data.missing[].expectedNights | Integer | 期望晚数;无该维度时 `null` |
|
||||
| data.missing[].actualNights | Integer | 实际晚数;无该维度时 `null` |
|
||||
| data.checkedResourceTypes | Array | 本次预检覆盖的资源类型,恒为 `["HOTEL","VEHICLE"]`;两类均已覆盖到逐户粒度 |
|
||||
| data.vehicleWaived | Boolean | 整团免车时为 `true`,此时车侧校验整体跳过、`vehicleMissing` 恒为空数组 |
|
||||
| data.vehicleMissing[] | Array | 车侧缺失清单,按校验顺序全部列出、不按户合并;`vehicleWaived=true` 时为空数组 |
|
||||
| data.vehicleMissing[].reason | String | **本次扩充**:车侧缺失原因码,新增 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,见六.5 |
|
||||
| data.vehicleMissing[].groupCode | String | 涉及的乘车分组编码;无分组维度时 `null` |
|
||||
| data.vehicleMissing[].tripDate | String | 涉及日期(`yyyy-MM-dd`);无日期维度时 `null` |
|
||||
| data.vehicleMissing[].orderId | String | 涉及的子订单 ID;无订单维度时 `null` |
|
||||
| data.vehicleMissing[].orderNo | String | 子订单号快照;订单不属于本团期时 `null` |
|
||||
| data.vehicleMissing[].detail | String | **本次报文变更**:人话描述,与整团确认抛出的错误报文逐字相同,可直接展示;户级条目不再带雪花 orderId |
|
||||
| data.groupVehicleRequirementId | String | 当前活跃正式用车需求主键;无则 `null` |
|
||||
| data.groupVehicleRequirementStatus | String | 活跃正式用车需求状态;无则 `null`。`DISPATCHED` / `DONE` 同属预检可通过的正常状态 |
|
||||
| data.groupVehicleRequirementVersion | Integer | 活跃正式用车需求版本号;无则 `null` |
|
||||
| data.transferSubmitEnabled | Boolean | 当前环境是否开放接送机需求提交;`false` 时提交 `kind=TRANSFER` 会被 809004 拒 |
|
||||
| data.transferDeclaredWithoutRequirement[] | Array | 声明了接送机却没报 TRANSFER 需求的户(提示性,不进 `ready`、不阻断确认);无此类户时为空数组、不是 `null` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2100856430494973953/requirement/confirm-check HTTP/1.1
|
||||
Host: <网关地址>
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2100856430494973953",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusName": "资源准备中",
|
||||
"ready": false,
|
||||
"missing": [
|
||||
{
|
||||
"orderId": "2102309919002943489",
|
||||
"orderNo": "HL20260922161158291",
|
||||
"customerName": "王二麻子",
|
||||
"consultantId": "2083111674486693889",
|
||||
"consultantName": "刘畅",
|
||||
"reason": "NOT_SUBMITTED",
|
||||
"reasonName": "未提报",
|
||||
"dayNumber": null,
|
||||
"segmentIndex": null,
|
||||
"expectedNights": null,
|
||||
"actualNights": null
|
||||
}
|
||||
],
|
||||
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
|
||||
"vehicleWaived": false,
|
||||
"vehicleMissing": [
|
||||
{
|
||||
"reason": "GROUP_REQUIREMENT_NOT_FOUND",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": null,
|
||||
"orderNo": null,
|
||||
"detail": "团期「第3期 10月8日出发团」尚未形成正式用车需求"
|
||||
},
|
||||
{
|
||||
"reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": "2102309919002943489",
|
||||
"orderNo": "HL20260922161158291",
|
||||
"detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务"
|
||||
}
|
||||
],
|
||||
"groupVehicleRequirementId": null,
|
||||
"groupVehicleRequirementStatus": null,
|
||||
"groupVehicleRequirementVersion": null,
|
||||
"transferSubmitEnabled": true,
|
||||
"transferDeclaredWithoutRequirement": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
需求齐备、可以整体确认时,两份清单都是空数组(不是 `null`),`ready` 为 `true`;字段集与上例完全相同:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2100856430494973953",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusName": "资源准备中",
|
||||
"ready": true,
|
||||
"missing": [],
|
||||
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
|
||||
"vehicleWaived": false,
|
||||
"vehicleMissing": [],
|
||||
"transferDeclaredWithoutRequirement": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
无需求确认权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 本接口只读,任何情况下不写库,可安全重复调用。
|
||||
- `vehicleMissing` **不按户合并、不短路**:团级的 `GROUP_REQUIREMENT_NOT_FOUND` 与户级的 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` 会同时出现(实测响应即如此)。前者说「团级需求没形成」,后者说「是谁挡着它形成」,两条的处置对象不同,都要渲染出来。
|
||||
- 区分条目是团级还是户级的唯一结构化判据是 `orderId` / `orderNo` 是否为 `null`,**不要靠解析 `detail` 文本**。户级条目恒带 `orderId` 与 `orderNo`。
|
||||
- `missing` 为空但 `ready=false` 时,原因一定在 `vehicleMissing`;`vehicleWaived=true` 时车侧整体跳过,`vehicleMissing` 恒为空数组,这是合法逃生口不是异常。
|
||||
- `transferDeclaredWithoutRequirement` 是提示性名单,不进 `ready`、不阻断确认,也不并进 `vehicleMissing`;`transferSubmitEnabled=false` 时这份名单**照报**,此时该提示「当前环境未开放接送机需求提交」而不是隐藏名单。
|
||||
- 预检通过不等于确认一定成功:确认时的 CAS 冲突(809101 / 809112)只在写入阶段才可能发生,整团回滚、零写入。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写**后端返回什么、前端据什么判定**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误的判定写法
|
||||
|
||||
| 场景 | 判定写法 |
|
||||
|------|----------|
|
||||
| ✅ 判「该户没提交房需求」 | `hotelRequirementStatus === null && hotelRequirementStatusName === '未提交'`,或直接 `hotelRequirementStatus == null` |
|
||||
| ✅ 判「该户不需要房」 | `hotelRequirementStatus == null && hotelRequirementStatusName == null` |
|
||||
| ✅ 渲染状态列 | `statusName ?? status ?? '—'`(三级兜底,`null` 安全) |
|
||||
| ❌ 判「没提交」用 `status === 'PENDING'` | `PENDING` 现在唯一含义是「已提交、等资源侧接单」,这样写会把真正在排队的户也算成未提交 |
|
||||
| ❌ 从 `vehicleMissing[].detail` 里正则抠订单号 | 报文已改写,户级条目的 `detail` 里不再有 ID;订单号读 `vehicleMissing[].orderNo` |
|
||||
| ✅ 判车侧缺失条目是团级还是户级 | `item.orderId == null` → 团级(跳去团级用车需求编辑);否则户级(跳到该子订单) |
|
||||
| ❌ 假设 `vehicleMissing` 至多一条 | 团级与户级条目会并列出现,数组长度可以大于 1 |
|
||||
|
||||
### 报文变更涉及的调用口
|
||||
|
||||
下列写口在失败时抛出的 `message` 与预检 `detail` 同源,本次一起改变;它们的**请求契约、字段名、错误码码值都没有变**,变的只是给人看的那段文本:
|
||||
|
||||
| 方法 | 路径 | 涉及错误码 | 弹窗场景 |
|
||||
|------|------|-----------|----------|
|
||||
| POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 809100 / 809103-809110 / 809122 / 809007 | 点「整体确认需求」,车侧不过时弹第一条违规的报文(零写入) |
|
||||
| PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 809107 / 809108 / 809109 / 809115 | 保存/提交乘车分组被校验拒绝 |
|
||||
| POST | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/waive` | 809114 | 声明整团免车时车务已开工被拒 |
|
||||
| GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 同上全部 | 预检横幅里的 `vehicleMissing[].detail` |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本次两个变更接口都是只读端点,**零写入**。
|
||||
|
||||
| 前端动作 | 写库行为 |
|
||||
|----------|----------|
|
||||
| 调 `GET .../orders` | 无 |
|
||||
| 调 `GET .../requirement/confirm-check` | 无 |
|
||||
| 调 `POST .../requirement/confirm` 且车侧校验不过(含新增的 809122) | 无:阶段守卫 → 住宿缺失(589533)→ 车侧校验的顺序全部排在任何写入之前,抛出即整笔事务零写入 |
|
||||
|
||||
新增的 809122 不引入任何新表、新列、新状态值;它是对既有「该户有没有 active 行程用车需求行」的一次查询结果的表达。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截,不进业务码集)。
|
||||
- 团期不存在 → HTTP 200 + `code=589500`。
|
||||
- 无权限 / 团期不在本人名下 → HTTP 200 + `code=589507`。
|
||||
- 老数据兼容:历史上没有 active 需求行的户,改前读出 `"PENDING"`、改后读出 `null`,**存量数据不迁移**,同一条记录在两个版本上读数不同,差异来自读取时的回落逻辑而非库里的值。
|
||||
- 枚举外未知状态码:`*StatusName` 回落原 code,不抛异常、不返空串。
|
||||
- 该户不需要房 / 不需要车时,对应的 `*Status` 与 `*StatusName` **都**是 `null`,前端渲染「—」。
|
||||
- `vehicleMissing` 与 `transferDeclaredWithoutRequirement` 在「一条都没有」时是空数组,不是 `null`。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### hotelRequirementStatus / vehicleRequirementStatus(RequirementStatus)
|
||||
|
||||
**所属字段**: `GroupBatchOrderItemRespVO.hotelRequirementStatus` / `.vehicleRequirementStatus` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(房 / 车) | 说明 |
|
||||
|----|------|------|
|
||||
| `null` | 未提交 / `null` | **本次新增取值**:该户没有 active 需求行。该户需要这项资源 → `*StatusName` 为「未提交」;不需要 → `*StatusName` 也是 `null` |
|
||||
| `PENDING` | 待房务配 / 待车队配 | 已提交并放行,等资源侧接单 |
|
||||
| `PROCESSING` | 配房中 / 配车中 | 资源侧处理中 |
|
||||
| `DONE` | 配房完成 / 配车完成 | 资源侧完成 |
|
||||
| `PENDING_REVIEW` | 待审核 / 待提交车务 | 定制师已报、等管理员动作;车侧本列恒为行程用车(TRAVEL),故用「待提交车务」(#8218) |
|
||||
| `REJECTED_TO_CONSULTANT` | 已驳回定制师 | 打回定制师重提 |
|
||||
| `REJECTED_TO_ADMIN` | 已驳回管理员 | 打回管理员 |
|
||||
|
||||
### vehicleMissing[].reason(车侧缺失原因)
|
||||
|
||||
**所属字段**: `GroupBatchRequirementCheckRespVO.VehicleMissingItem.reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `GROUP_REQUIREMENT_NOT_FOUND` | 团级正式用车需求未形成 | 809100;团级维度,`orderId` / `orderNo` 为 `null` |
|
||||
| `GROUP_REQUIREMENT_STATUS_INVALID` | 团级需求状态不允许确认 | 809101 |
|
||||
| `NO_GROUP` | 至少要提交一个乘车分组 | 809103 |
|
||||
| `GROUP_CODE_INVALID` | 乘车分组编码重复或改名 | 809104 |
|
||||
| `DAY_OUT_OF_GROUP_RANGE` | 逐日行不在本组服务日范围内 | 809105 |
|
||||
| `DAY_GAP_IN_GROUP_RANGE` | 缺某日的逐日用车人数 | 809106 |
|
||||
| `MEMBER_FOREIGN_ORDER` | 成员子订单不属于本团期 | 809107 |
|
||||
| `MEMBER_DUPLICATE_DAY` | 同一户同一日落在两个分组 | 809108 |
|
||||
| `ORDER_DAY_UNCOVERED` | 某子订单某日没有被任何分组覆盖 | 809109 |
|
||||
| `HEADCOUNT_LESS_THAN_MEMBERS` | 逐日用车人数小于当日成员户数 | 809110 |
|
||||
| `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` | 该户尚未提交行程用车需求 | **本次新增**,809122;户级维度,恒带 `orderId` / `orderNo`;处置是催该户定制师提交 |
|
||||
| `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` | 待放行接送机需求未回填服务日 | 809007;只带 `orderId` |
|
||||
|
||||
### missing[].reason(住宿缺失原因)
|
||||
|
||||
**所属字段**: `GroupBatchRequirementCheckRespVO.MissingItem.reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `NOT_SUBMITTED` | 未提报 | 该户需房但没有 active 房需求行(含被打回后未重提) |
|
||||
| `ROOM_CATEGORY_MISSING` | 缺房型 | 需求行存在但房型行缺失 |
|
||||
| `INVALID_REQUIREMENT` | 需求结构不合法 | days 结构不可用 |
|
||||
| `NIGHTS_MISMATCH` | 晚数不符 | 实际晚数与期望晚数不一致 |
|
||||
| `DAY_NUMBER_INVALID` | 天序不合法 | dayNumber 越界或重复 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `GroupBatchOrderItemRespVO.hotelRequirementStatus` | 无 active 房需求行时回落 `"PENDING"` | 无 active 行时为 `null` |
|
||||
| `GroupBatchOrderItemRespVO.hotelRequirementStatusName` | 相应显示「待房务配」 | code 为 `null` 时:该户需房 → `"未提交"`;不需房 → `null` |
|
||||
| `GroupBatchOrderItemRespVO.vehicleRequirementStatus` | 无 active 车需求行时回落 `"PENDING"` | 无 active 行时为 `null` |
|
||||
| `GroupBatchOrderItemRespVO.vehicleRequirementStatusName` | 相应显示「待车队配」 | code 为 `null` 时:该户需车 → `"未提交"`;不需车 → `null` |
|
||||
| `GroupBatchRequirementCheckRespVO.vehicleMissing[].reason` | 11 个取值 | 追加 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,共 12 个 |
|
||||
| `GroupBatchRequirementCheckRespVO.checkedResourceTypes` | 值仍是 `["HOTEL","VEHICLE"]`,但车侧只覆盖团级 | 值不变;车侧实际覆盖扩到逐户(这是把字段宣称的覆盖补齐,不是取值变化) |
|
||||
|
||||
### 报文变更(六个错误码的渲染文本)
|
||||
|
||||
| 错误码 | 改前渲染 | 改后渲染 |
|
||||
|--------|----------|----------|
|
||||
| 809100 | `团期 2100856430494973953 尚未形成正式用车需求` | `团期「第3期 10月8日出发团」尚未形成正式用车需求`;团期名与期号快照都缺时为 `该团期尚未形成正式用车需求` |
|
||||
| 809107 | `子订单 2102309919002943489 不属于本团期,不能作为乘车成员` | `该子订单不属于本团期,不能作为乘车成员` |
|
||||
| 809108 | `子订单 2102309919002943489 在 2026-10-09 同时属于分组 BUS、SUV,同一户同一日只能属于一个分组` | `该子订单在 2026-10-09 同时属于分组 BUS、SUV,同一户同一日只能属于一个分组` |
|
||||
| 809109 | `子订单 2102309919002943489 的 2026-09-13 没有被任何乘车分组覆盖` | `该子订单的 2026-09-13 没有被任何乘车分组覆盖` |
|
||||
| 809114 | `车务已开工(团期 2100856430494973953:团级配车已就绪)` / `车务已开工(子订单 2102309919002943489:用车需求已到 DISPATCHED)` | `车务已开工(团期「第3期 10月8日出发团」:团级配车已就绪)` / `车务已开工(子订单 HL20260922161158291:用车需求已到 DISPATCHED)`;取不到订单号时为 `某子订单` |
|
||||
| 809115 | `团期 2100856430494973953 已声明整团免车,提交乘车分组前请先整份撤回(withdraw)回草稿` | `团期「第3期 10月8日出发团」已声明整团免车,提交乘车分组前请先整份撤回(withdraw)回草稿` |
|
||||
|
||||
**码值一个都没变**,变的只是 `message` / `detail` 的文本。809107 / 809108 / 809109 是直接删掉标识符:这三条只出现在预检清单上,而清单行已经由 `orderNo` 字段结构化带了订单号,文本里再带一个雪花 ID 就是同一行上的第二个标识符。809114 保留标识符只把 ID 换成订单号:它只落在免车声明弹窗上,弹窗外没有第二处说明是哪一户。
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团里有户一条需求都没提交时的列表显示 | 「待房务配 / 待车队配」(与同屏预检横幅的「未提报」矛盾) | 「未提交」 |
|
||||
| 车侧预检对「某户一行行程用车需求都没提交」 | 无任何表达位置,整团确认只报团级的 809100 | `vehicleMissing` 里多一条 809122,带 `orderId` / `orderNo` |
|
||||
| 团级缺席时是否还检查户级 | 团级缺席即返回,不再往下查 | 两层并列输出,团级缺席不短路户级 |
|
||||
| 整团确认在户级未提交时的结果 | 报 809100(运营据此去催车务,方向错了) | 报 809122(催该户定制师提交),零写入 |
|
||||
| 车务报文里的标识符 | 雪花 ID | 团期可读名称 / 订单号 / 直接省略 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是(部分)。`*Status` 新增 `null` 取值;对 `detail` / `message` 做文本匹配或抠 ID 的逻辑会失效。字段名、字段数量、错误码码值、请求契约均未变。
|
||||
- **前端是否必须同步上线**: 否。已核对 `hl-ui origin/v2.1` 的 `src/views/order-v2/batch/detail/components/RequirementTab.vue`:状态列渲染为 `r.hotelRequirementStatusName || r.hotelRequirementStatus || '—'`(`:812-814`),对 `null` 安全;车侧缺失清单按 `item.orderNo` 是否存在分流(`:654` `vehicleReqMissing`、`:708-714` `onVehicleMissingClick`),809122 条目带 `orderNo`,会被正确归为户级并跳到该子订单。当前前端不改也能正确工作。
|
||||
- **前端 workaround 清理点**: 若页面上有「把 `PENDING` 当作未提交」的本地兜底判断,可以撤掉——后端现在用 `null` 表达「未提交」,语义是唯一的。
|
||||
- **风险面**: 唯一需要人工确认的是「是否存在对 `vehicleMissing[].detail` 做字符串解析的代码」。上述两个消费点都只做原样展示,未发现解析逻辑。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台「团期详情 → 查看需求」tab 的子订单状态列、整团确认预检横幅、车务相关弹窗文案。
|
||||
- **零影响**:
|
||||
- 小程序端(mp)全部接口:本次改动只落在 `/v3/admin/` 前缀下。
|
||||
- `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`(用房·汇总 / 用车·汇总):字段与取值一个没动。该页此前的「汇总加载失败」已定位为前端同 tick 重复请求被去重拦截器 abort 所致,mmg 已在 `hl-ui 4b33ccbd9` 修复并已在测试环境前端 dist 里(`2026-09-23 18:06` 构建),与本次后端改动无关。
|
||||
- 房需求的提交 / 打回 / 放行链路:`missing[]` 的口径、589533 的触发条件与报文一律不变。
|
||||
- 接送机(TRANSFER)相关:`transferSubmitEnabled`、`transferDeclaredWithoutRequirement`、809007 的语义与取值不变。
|
||||
- 订单列表 / 订单详情 / 金额相关接口:未触及。
|
||||
- 历史数据:存量不迁移;差异只来自读取时的回落逻辑。
|
||||
- 数据库:无 DDL、无 Flyway 脚本、无新列。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:`hl-order-service-v3` @ `dev-v3` `fc81fff1f`,jar 构建时间 `2026-09-23 18:36:51`,两个实例(8086 / 8186)均 UP。以下读数经网关实测:
|
||||
|
||||
```
|
||||
GET /v3/admin/order/group-batch/2100856430494973953/orders
|
||||
→ 200;26-2355(HL20260922161158291)hotelRequirementStatus=null / StatusName=未提交,
|
||||
vehicleRequirementStatus=null / StatusName=未提交 ✓
|
||||
→ 200;26-7060(HL20260918155619496)hotelRequirementStatus=PENDING_REVIEW / 待审核,
|
||||
vehicleRequirementStatus=PENDING_REVIEW / 待提交车务(无回归)✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2100856430494973953/requirement/confirm-check
|
||||
→ 200;vehicleMissing 里 GROUP_REQUIREMENT_NOT_FOUND 与
|
||||
HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED 两条同时出现(未被团级缺席短路)✓
|
||||
→ 809100 的 detail 已是「团期「第3期 10月8日出发团」尚未形成正式用车需求」(无雪花 ID)✓
|
||||
→ 809122 的条目带 orderId=2102309919002943489 / orderNo=HL20260922161158291 ✓
|
||||
→ checkedResourceTypes=["HOTEL","VEHICLE"]、vehicleWaived=false、ready=false ✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2100856430494973953/requirement-summary
|
||||
→ 200 × 3 次(3.56s / 2.11s / 2.55s),服务端日志零 ERROR / Exception ✓
|
||||
```
|
||||
|
||||
验证团期: `groupBatchId=2100856430494973953`(第3期 10月8日出发团,2 户活跃子订单)
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8249](https://git.1814.love:8443/wx/HL/issues/8249)
|
||||
- 关联 PR: [wx/HL#8286](https://git.1814.love:8443/wx/HL/pulls/8286)
|
||||
- 同页状态文案的上一次分叉(`PENDING_REVIEW` 车侧按 kind 分叉): `changelogs-v2/2026-09/23_8218_行程用车状态文案按kind分叉-修改接口-管理后台.md`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8249](https://git.1814.love:8443/wx/HL/issues/8249)
|
||||
- **PR**: [#8286](https://git.1814.love:8443/wx/HL/pulls/8286)
|
||||
- **Merge commit**: [fc81fff1f](https://git.1814.love:8443/wx/HL/commit/fc81fff1fd21e4a002c417041ed3fbb5bc2ce96a)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,400 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8252"
|
||||
title: "订单列表新增团期关键词筛选,团单行新增团期名与期次展示字段"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "5f27c0094745c60cde7de866bb3450eee61fd91d"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "GET /v3/admin/order 新增入参 groupBatchKeyword(长度上限 64),出参 OrderListItemRespVO 新增 groupBatchName 与 groupBatchLabel。前端需在订单列表增加团期关键词筛选,并在团单行产品名后渲染「第N期 团期名」,故 frontend_status 记 pending。前端已交付(2026-09-23):列表头新增「团期」关键词输入框原样透传 groupBatchKeyword(maxlength 64 输入侧截断;清空省略=不传;六页签计数同口径透传);ProductCell 产品名后渲染「第N期 团期名」,label/name 逐段 trim 判空省略、全空整行不渲染,判团单仍用 groupOrder/groupBatchId。新建 spec 8 例+存量 6 例回归全绿,checkpoint 全量含生产构建通过。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单列表团期筛选与团单行团期展示(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8261
|
||||
> **Issue**: #8252
|
||||
> **日期**: 2026-09-23
|
||||
> **合并提交**: b6e84bdd9
|
||||
> **影响范围**: 管理后台订单列表的筛选条件(新增团期关键词)与团单行展示(新增团期名 + 期次)
|
||||
|
||||
---
|
||||
|
||||
## 一、接口背景
|
||||
|
||||
订单列表原本只支持按 `groupBatchId` 筛团期,而那是 19 位雪花 ID:运营在团期看板上看到的是「第3期 10月8日出发团」这样的文案,
|
||||
记不住、也没法粘贴到订单列表去筛。团期看板自己支持按团期名 / 期次搜索,订单列表不支持,同一句话在两个页面搜出不同结果。
|
||||
|
||||
本次给订单列表补上同样口径的团期关键词筛选,并把团期名与期次直接补到团单行上,运营从看板跳到订单列表后能直接看到自己在看哪个团期。
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 管理后台订单列表 | GET | `/v3/admin/order` | 修改接口 | 入参新增 `groupBatchKeyword`;出参 `OrderListItemRespVO` 新增 `groupBatchName` 与 `groupBatchLabel`;其余入参、其余出参、权限码、错误码均未变 |
|
||||
|
||||
> 同一个 Controller 方法同时挂载集合根与 `/v3/admin/order/list` 两条路径(`@GetMapping({"", "/list"})`),
|
||||
> 两个地址的入参、出参、错误码、数据权限完全一致,本条目统称「订单列表接口」。
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 管理后台订单列表 `GET /v3/admin/order`
|
||||
|
||||
**VO**: `OrderListItemRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「订单管理」列表页:首次进入、翻页、切换六个页签、修改任意筛选项时调用。
|
||||
本次与该页相关的两件事:列表头部新增「团期」关键词输入框(按团期名 / 期次 / 团号搜)。
|
||||
团单行的产品名后面渲染「第N期 团期名」,数据由本接口出参的 `groupBatchName` 与 `groupBatchLabel` 提供。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchKeyword | Query | String | 否 | 长度 ≤64;全角空格与 NBSP 归一后为空串等同不传;groupBatchId 非空时本参数被忽略 | **本次新增**。团期关键词:按「团号模糊 ∪ 团期名模糊 ∪ 期次精确」解析出命中的团期后,只返回挂在这些团期下的订单。支持照抄页面渲染的文案,如 `第3期 10月8日出发团`。非空时强制按团单口径筛(等价于 orderKind=GROUP) |
|
||||
| groupBatchId | Query | Long | 否 | 19 位正整数 | 运营团期 ID(既有字段,本次未改)。非空时本参数优先:groupBatchKeyword 被忽略,且强制按团单口径筛 |
|
||||
| orderKind | Query | String | 否 | ALL / GROUP / NORMAL;缺省 NORMAL | 订单归属类型(既有字段,本次未改)。传了 groupBatchId 或非空 groupBatchKeyword 时,本参数被后端的团期口径覆盖为 GROUP |
|
||||
| pageNo | Query | Integer | 否 | ≥1,缺省 1 | 页码(既有字段)。等价别名 page,两者写哪个都生效 |
|
||||
| pageSize | Query | Integer | 否 | 1~100,缺省 20 | 每页条数(既有字段) |
|
||||
| statusGroup | Query | String | 否 | ALL / BEFORE_TRIP / ON_TRIP / SETTLEMENT / ABNORMAL / AFTERSALE | 页签分组(既有字段)。与 orderStatus 下拉为 AND 关系 |
|
||||
| orderStatus | Query | String | 否 | PENDING_PAY / CUSTOMIZING / PENDING_DEPARTURE / TRAVELLING / COMPLETED / CANCELLED,多值用逗号 | 粗状态过滤(既有字段) |
|
||||
| flowStatus | Query | String | 否 | 细状态枚举,多值用逗号 | 细状态过滤(既有字段) |
|
||||
| keyword | Query | String | 否 | 无 | 通用关键词:团号 / 客户姓名 / 产品名 / 订单号 任一 LIKE(既有字段,与本次的团期关键词是两个独立参数) |
|
||||
| tagNames | Query | List<String> | 否 | 多标签 OR,任一命中即返 | 标签过滤(既有字段) |
|
||||
| consultantName | Query | String | 否 | LIKE 匹配 | 定制师姓名(既有字段) |
|
||||
| departureDateFrom | Query | Date | 否 | yyyy-MM-dd | 出发日期区间起(既有字段) |
|
||||
| departureDateTo | Query | Date | 否 | yyyy-MM-dd,早于起日返 100001 | 出发日期区间止(既有字段) |
|
||||
| createSource | Query | String | 否 | CONSULTANT / CUSTOMER / 其他来源枚举 | 订单来源过滤(既有字段) |
|
||||
| cancelled | Query | Boolean | 否 | 缺省 false | 是否包含已取消订单(既有字段) |
|
||||
|
||||
#### 出参 `Result<PageResult<OrderListItemRespVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| records | Array | 订单行列表,元素结构见下方「行内字段」 |
|
||||
| total | Integer | 命中总条数(不受 pageSize 影响) |
|
||||
| page | Integer | 当前页码 |
|
||||
| pageSize | Integer | 每页条数 |
|
||||
| records[].groupBatchName | String | **本次新增**。团期名称快照(取自下方「业务边界」所述的下单时快照口径)。非团单与团期已软删(解散)两种情况均为 null,不返空串 |
|
||||
| records[].groupBatchLabel | String | **本次新增**。期次序号裸数字串,如 `"3"`,不含「第」「期」二字,由前端拼成「第3期」。存量未刷新快照的团期可能为 null |
|
||||
| records[].groupBatchId | Long | 既有字段,本次未改。运营团期 ID(JSON 中为字符串)。非空即团单,这是判别团单的唯一依据 |
|
||||
| records[].groupOrder | Boolean | 既有字段,本次未改。是否团单,与 groupBatchId 同源(groupBatchId != null 的派生值) |
|
||||
| records[].productName | String | 既有字段,本次未改。产品名快照。「第N期 团期名」渲染在产品名之后 |
|
||||
| records[].orderNo | String | 既有字段,本次未改。订单号(创单瞬间生成,永不变) |
|
||||
| records[].id | Long | 既有字段,本次未改。订单 ID(JSON 中为字符串) |
|
||||
| records[].customerName | String | 既有字段,本次未改。客户姓名 |
|
||||
| records[].departureDate | Date | 既有字段,本次未改。出发日,未定日期时为 null |
|
||||
| records[].orderStatus / orderStatusName | String | 既有字段,本次未改。粗状态枚举值与中文名 |
|
||||
| records[].flowStatus / flowStatusName | String | 既有字段,本次未改。细状态枚举值与中文名 |
|
||||
| records[].totalAmount / payableAmount / paidAmount / refundAmount / balanceAmount | BigDecimal | 既有字段,本次未改。金额在 JSON 中为字符串 |
|
||||
| records[].consultantName | String | 既有字段,本次未改。定制师姓名 |
|
||||
|
||||
> 单行 VO 本次从 44 个字段变为 46 个:新增 `groupBatchName`、`groupBatchLabel`,其余 44 个字段逐字未变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order?pageNo=1&pageSize=20&orderKind=ALL&groupBatchKeyword=%E7%AC%AC3%E6%9C%9F+10%E6%9C%888%E6%97%A5%E5%87%BA%E5%8F%91%E5%9B%A2 HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <admin token>
|
||||
Accept: application/json
|
||||
|
||||
# 无请求体。
|
||||
# 关键词解码后为「第3期 10月8日出发团」;等价别名地址:GET /v3/admin/order/list(参数与响应逐字一致)。
|
||||
# 只按团期名筛:GET /v3/admin/order?pageNo=1&pageSize=20&groupBatchKeyword=%E4%BA%91%E5%8D%97
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "2102309919002943489",
|
||||
"orderNo": "HL20260922161158291",
|
||||
"teamNo": "26-2355",
|
||||
"productName": "王骁测试团期产品",
|
||||
"productType": "GROUP",
|
||||
"productTypeName": "小蒙马",
|
||||
"productCoverImg": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/material/2026/03/07/ed278d1da74c3827cb06b7f0cf6091fe.jpg",
|
||||
"tierName": "轻奢",
|
||||
"customerName": "王二麻子",
|
||||
"customerPhoneMasked": "185****0000",
|
||||
"peopleSummary": "4 大",
|
||||
"departureDate": "2026-10-08",
|
||||
"tripDays": 3,
|
||||
"tripNights": 2,
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"orderStatusName": "定制中",
|
||||
"flowStatus": "AWAITING_PROFILE",
|
||||
"flowStatusName": "待补全信息",
|
||||
"flowStep": 1,
|
||||
"flowStepTotal": 6,
|
||||
"flowStepCode": "PROFILE",
|
||||
"flowStepName": "补全信息",
|
||||
"currentSubFlows": null,
|
||||
"totalAmount": "15920.00",
|
||||
"payableAmount": "15920.00",
|
||||
"paidAmount": "2000.00",
|
||||
"refundAmount": "0.00",
|
||||
"balanceAmount": "13920.00",
|
||||
"consultantName": "刘畅",
|
||||
"createSource": "CONSULTANT",
|
||||
"createSourceLabel": "定制师创建",
|
||||
"tags": [],
|
||||
"createdAt": "2026-09-22 16:11:58",
|
||||
"depositAmount": "2000.00",
|
||||
"depositRatio": null,
|
||||
"paymentMode": "DEPOSIT",
|
||||
"singleRoomSurcharge": "0.00",
|
||||
"agencyId": "2051922156798779394",
|
||||
"refundPolicyId": "2047249965792579585",
|
||||
"productSubtitle": "发短信给对方搞定",
|
||||
"aftersaleStatus": "NONE",
|
||||
"aftersaleStatusName": "无售后",
|
||||
"groupBatchId": "2100856430494973953",
|
||||
"groupOrder": true,
|
||||
"groupBatchName": "10月8日出发团",
|
||||
"groupBatchLabel": "3"
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"pageSize": 5
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
两种空值形态语义不同,前端要分别处理。
|
||||
|
||||
其一:关键词没有任何命中团期时,接口返 200 与空页(`records: []`、`total: 0`),同时六个页签的计数一并归零——列表数字与页签数字不会各说各话。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"pageSize": 5
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
其二:行本身存在,但该行拿不到团期信息。非团单行、以及团期已软删(解散)的团单行,`groupBatchName` 与 `groupBatchLabel` 都是 `null`(不是空串)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "2097270761474469890",
|
||||
"orderNo": "HL20260908182809530",
|
||||
"productName": "测试小蒙马-多档-固定金额",
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"groupBatchId": "2097270759599616002",
|
||||
"groupOrder": true,
|
||||
"groupBatchName": null,
|
||||
"groupBatchLabel": null
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"page": 1,
|
||||
"pageSize": 5
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100001,
|
||||
"message": "团期关键词长度不能超过 64",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 100001 | groupBatchKeyword 超过 64 个字符(注意:不是 HTTP 400,是业务码 100001,HTTP 状态仍为 200) |
|
||||
| 100001 | groupBatchId 传了非数字(既有行为,本次未改) |
|
||||
| 100001 | pageNo 小于 1、pageSize 超出 1~100、orderKind 传非法值、出发日区间倒置 |
|
||||
| 401 | 未登录(网关拦截) |
|
||||
| 403 | 缺少管理端订单列表访问权限 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **团期名与期次都取自 order 侧快照**:`groupBatchName` / `groupBatchLabel` 读的是订单侧记录的团期快照,而团期看板展示的是 product 侧实时重算值;快照只在该团期有新订单进入时刷新,**两者可能不一致**。这是两个页面各自的既定口径,不是本接口的缺陷。
|
||||
- **`groupBatchLabel` 可能为 null**:存量未刷新的团期拿不到期次,此时只渲染团期名,**不得渲染「第null期」「第期」**。`groupBatchName` 同理,为 null 时整段不渲染。
|
||||
- **非团单与「团期已软删(解散)」两种情况下两字段均为 null**(不是空串);若团期名本身是空字符串的脏数据,则返回 `""`,前端需按空文案处理而不是渲染出「第N期 」这样带空格残缺文案。
|
||||
- **软删团期的历史订单仍能按团单筛出来**(`groupOrder=true`、`groupBatchId` 非空),只是团期名与期次为 null——这是既定行为,前端不要据此把行判成普通订单。
|
||||
- **本接口的 `groupBatchName` 是团期名快照,与下单接口(订单创建响应)里那个恒为 null 的 `groupBatchName` 不是同一个语义**,不要把两个接口的字段名合并成同一个前端模型复用。
|
||||
- **关键词口径与团期看板一致**:团号 LIKE ∪ 团期名 LIKE ∪ 期次精确匹配,且支持「第3期 云南」这类复合输入(期次精确 AND 名称/团号模糊)。期次是精确匹配,「第1期」不会连带命中第 10、11、21 期。
|
||||
- **命中团期超过 500 个时结果被截断**(只取前 500 个团期对应的订单),属公开的失败模式:关键词过宽时 total 会偏小。
|
||||
- **命中为空时返回空页且六个页签计数同步为 0**;不传 `groupBatchKeyword` 时,本接口行为与本次改动前完全一致(含只传一个全角空格或 NBSP 的情况,逐项等同不传)。
|
||||
- **权限码零变化**:定制师等非管理员角色仍只看本人名下订单,权限口径与本次改动前逐字一致,访问本接口不会出现 589507。
|
||||
- **已知差异(契约边界)**:某些团期名本身形如「3期特惠」时,团期看板按整串匹配,而订单列表会把它解析成「期次 3 + 名称 特惠」,因此同一个输入在两个页面可能给出不同结果。
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 查询串 |
|
||||
|---|---|
|
||||
| ✅ 按团期名筛 | `groupBatchKeyword=10月8日出发团`,返回挂在该团期下的订单 |
|
||||
| ✅ 按期次筛 | `groupBatchKeyword=第3期`,期次精确匹配,不含第 10、11、21 期 |
|
||||
| ✅ 复合输入(照抄页面文案) | `groupBatchKeyword=第3期 云南深度8日`,拆成「期次 = 3」且「名称含 云南深度8日」 |
|
||||
| ✅ 只填一个全角空格 | `groupBatchKeyword=%E3%80%80`,等同于不传,行为与改动前逐项一致 |
|
||||
| ✅ 精确跳转(从团期看板带 ID 跳过来) | `groupBatchId=2100856430494973953` |
|
||||
| ❌ 两个参数都传且互不匹配 | `groupBatchId=<团期ID>&groupBatchKeyword=不存在的团期名` → 以 ID 为准,关键词被忽略 |
|
||||
| ❌ 关键词超过 64 字符 | 65 个字符 → 100001「团期关键词长度不能超过 64」 |
|
||||
| ❌ 把期次写成中文数字 | `groupBatchKeyword=第三期` → 不是期次形态,按团期名/团号整串模糊匹配,通常零命中 |
|
||||
|
||||
### 前端渲染规则
|
||||
|
||||
- 团单行文案 = 产品名 + 「第{groupBatchLabel}期 {groupBatchName}」,任一段为 null 或空串时该段整段省略,不要留下多余空格。
|
||||
- 判断一行是不是团单请继续读 `groupOrder` / `groupBatchId`,不要用 `groupBatchName != null` 反推(软删团期的团单两字段为 null)。
|
||||
- 筛选框可以同时展示「团期名」与「第N期」两种写法;提交时把用户输入原样传给 `groupBatchKeyword` 即可,后端负责解析。
|
||||
- 筛选框在提交前建议做一次前端长度校验(≤64),避免让用户提交后只拿到一个 100001。
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
零数据库变更。本次不新增表、列、索引或数据迁移,也不写入任何数据:
|
||||
`groupBatchKeyword` 的匹配是对既有团期数据的只读查询,出参两字段是对既有订单侧快照的只读回填,整页一次批量查询,不产生写操作。
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 无管理端权限 → 403。
|
||||
- `groupBatchKeyword` 超过 64 字符 → 业务码 100001,HTTP 状态仍是 200。
|
||||
- `groupBatchId` 传非数字 → 业务码 100001(既有行为)。
|
||||
- 关键词无命中团期 → 200 + 空页,且六个页签计数全为 0。
|
||||
- 班期 / 团期数据在 order 侧无快照(非团单、软删团期)→ 两个新字段为 null,不返空串。
|
||||
- 团期名本身是空字符串的脏数据 → 返回 `""`。
|
||||
- 排期区间倒置(departureDateFrom > departureDateTo)→ 100001。
|
||||
- pageSize 超过 100 → 100001。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `OrderListReqVO.groupBatchKeyword` | 不存在 | 新增,String,≤64,Query 参数 |
|
||||
| `OrderListItemRespVO.groupBatchName` | 不存在(前端取到 undefined) | 返回团期名快照;非团单 / 软删团期为 null |
|
||||
| `OrderListItemRespVO.groupBatchLabel` | 不存在(前端取到 undefined) | 返回期次裸数字串,如 `"3"`;存量未刷新快照为 null |
|
||||
| 单行 VO 字段数 | 44 | 46 |
|
||||
| 其余 44 个出参字段 | — | 逐字未变 |
|
||||
| 其余 15 个入参字段 | — | 逐字未变(新增参数是新增项,不改既有字段语义) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 按团期名 / 期次筛选 | 不支持,只能传 19 位 groupBatchId | 新增 groupBatchKeyword,口径与团期看板一致 |
|
||||
| 期次匹配方式 | — | 期次精确匹配(第1期不命中第 10、11、21 期) |
|
||||
| 两个团期参数同传 | 不存在第二个参数 | 以 groupBatchId 为准,关键词被忽略 |
|
||||
| 空白关键词 | — | 归一后为空等同于不传,行为与改动前逐项一致 |
|
||||
| 关键词无命中 | — | 空页 + 六页签计数全 0 |
|
||||
| 团单行展示 | 只有 groupBatchId / groupOrder | 追加 groupBatchName / groupBatchLabel 供渲染「第N期 团期名」 |
|
||||
| 权限码 / 数据权限 | 仅 ADMIN / SUPER_ADMIN 看全量,其余角色只看本人订单 | 逐字未变 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否。入参是新增的可选参数,出参是新增的可选字段;不传关键词、不读新字段的老调用方行为逐字未变。
|
||||
- **前端是否必须同步上线**:否,但需要适配:列表要加团期关键词筛选入口,团单行要渲染团期名 + 期次。老前端不改也不会报错,只是看不到本次的两个能力。
|
||||
- **性能**:团期关键词的匹配落在团期名 / 期次的模糊查询上,命中团期条数有 500 的上限,超过即截断;订单侧仍是既有的分页查询,团期名与期次按页一次批量回填,不会按行查询。
|
||||
- **前端 workaround 清理点**:若前端此前为了显示团期名而在列表页额外调用团期接口或按 groupBatchId 反查,可改用本接口的两个新字段;无此类逻辑则无需改动。
|
||||
- **回滚**:撤销 PR #8261 即可,无数据、无迁移、无配置残留。
|
||||
- **未覆盖**:本次只证明后端契约可用。页面上的筛选框与团期文案是否落位属前端实现侧;两个页面对同一输入的差异见「业务边界」末条。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台订单列表(筛选条件 + 团单行字段)。
|
||||
- **零影响**:
|
||||
- 团期看板的搜索口径与命中结果(本次只新增解析口,未改看板既有语义)
|
||||
- 订单详情的团期四字段(batchNo / batchName / groupBatchStatus / groupBatchId)
|
||||
- 下单接口(订单创建响应)的同名字段
|
||||
- 六个页签计数的计算口径(关键词无命中时同步归零属既定一致行为)
|
||||
- 权限码与数据权限收缩逻辑
|
||||
- 小程序端:本接口仅管理后台使用,未涉及
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-order-service-v3 `dev-v3 @ b6e84bdd9`(PR #8261 合并提交),2026-09-23 16:23~16:24 滚动部署完成,两实例健康。
|
||||
验证入口:`https://api.test.1814.love`(经网关调用,非直连服务端口)。
|
||||
|
||||
自动化验收脚本 19 项全部 PASS,摘要如下:
|
||||
|
||||
| 用例 | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| AC-1 | 按团期名 `10月8日出发团` 筛 | 200,total=2,两行 groupBatchName 均为「10月8日出发团」 |
|
||||
| AC-2 | 按期次 `第3期` 筛 | 200,total=2,命中期次集合 = ['3'](精确,不含 10 / 13 / 23 / 30) |
|
||||
| AC-3 | 按期次 `第1期` 筛 | 200,total=13,命中期次集合 = ['1'](不含 10 / 11 / 21) |
|
||||
| AC-4 | 裸数字 `3` 筛 | 200,total=406;第3期的 2 单是其中子集 |
|
||||
| AC-5 | 复合输入 `第3期 10月8日出发团` | 200,total=2,期次 = ['3'] |
|
||||
| AC-6 | 无命中 `不存在的团期名ZZZ9` | 200,空页,total=0 |
|
||||
| AC-7 | 页签计数与列表同口径 | 命中时 ALL=2、其余页签 0;无命中时六个页签全 0 |
|
||||
| AC-8 | 只填空格 vs 完全不传 | total 同为 708,首行订单号逐字一致 |
|
||||
| AC-9 | 定制师身份(CUSTOMIZER) | 200 而非 589507;不带关键词时 total=19 且定制师列只含本人 |
|
||||
| AC-10 | 65 字符关键词 | 100001「团期关键词长度不能超过 64」 |
|
||||
| AC-11 | 只传 groupBatchId | 200,total=2 |
|
||||
| AC-12 | 两参同传 | 与只传 ID 同结果,orderNo 序列一致 |
|
||||
| AC-13 | 团期看板口径回归 | 三组关键词的命中团期集合与基线一致 |
|
||||
| AC-14 | 团单行展示字段 | orderNo=HL20260922161158291,groupBatchName=10月8日出发团,groupBatchLabel=3 → 可渲染「第3期 10月8日出发团」 |
|
||||
| AC-15 | 团单 300 行扫描 | 无空串残缺值;groupBatchLabel 为 null 的行数为 0 |
|
||||
| AC-17 | 普通单 220 行 | 两个新字段全部为 null(非空串) |
|
||||
| AC-18 | 同订单不同 pageSize 一致性 | pageSize=1 与 20 的 groupBatchName 一致;页级一次批量回填(单测 times(1),逐行查询 never) |
|
||||
| AC-19 | 深翻页 | 第 2、3 页各 20 行均带团期名 |
|
||||
| AC-20 | 订单详情回归 | 团期四字段与基线逐字一致 |
|
||||
|
||||
在此之上补一组经网关的真实请求取证,覆盖脚本未包含的软删场景:
|
||||
|
||||
| 用例 | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| AC-16 | 团期已软删(解散)的历史订单,`groupBatchId=2097270759599616002` | 200,total=3;三行 groupOrder=true,groupBatchName 与 groupBatchLabel 均为 null |
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8252](https://git.1814.love:8443/wx/HL/issues/8252)
|
||||
- 关联 PR: [wx/HL#8261](https://git.1814.love:8443/wx/HL/pulls/8261)
|
||||
- 团期看板关键词口径出处:#7942
|
||||
- 订单列表 groupBatchId 冻结契约出处:#7635
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8252](https://git.1814.love:8443/wx/HL/issues/8252)
|
||||
- **PR**: [#8261](https://git.1814.love:8443/wx/HL/pulls/8261)
|
||||
- **Merge commit**: [b6e84bdd9](https://git.1814.love:8443/wx/HL/commit/b6e84bdd9)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,499 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8278"
|
||||
title: "团级用车分组座位充足性校验(809116)改为扣司机座,与子订单级/fleet 单车派车口径统一"
|
||||
consumer: "admin"
|
||||
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: "PR #8296 已合并 dev-v3(a0973867f)。部署:hl-order-service-v3 dev-v3 @ a0973867f,2026-09-23 21:09:02 部署,deploy-status 读数 BEHIND=0 STATE=ok,8086/8186 两实例 Nacos 健康。测试服网关真实 PUT 实测(groupBatchId=2102749115823919105,20 座大巴 × 2 辆,两次请求均晚于部署时刻):headcount=40 时返回 809116(扣司机座后可载 38 人,不足 40);headcount=38 时返回 200,remainingPassengerSeats=0。定向单测 mvn -o -pl hl-order-service-v3 -am test:17 个外层类 148/0/0(含 9 个 @ArchTest 载体),BUILD SUCCESS。存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(seats/count 均非空)的分组 3 组,按新口径重算全部仍满足要求,按新口径会被拒的活跃分组数为 0;生产环境二期尚未开放,无生产存量。gateway_status: verified —— 路径本就在既有 /v3/admin/** order-service-v3 路由下,本次零路由改动,且已通过真实网关实测。;前端判 not_required:809116 全仓零文本/占位符解析(整句 toast+violations detail 直显);余座负值三处消费不 clamp 不拦截;spec 无刚好坐满放行预期"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3 团期需求: 团级用车分组座位充足性校验改为扣司机座
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(团期需求域)
|
||||
> **PR**: #8296
|
||||
> **Issue**: #8278
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的团级正式用车需求编辑弹窗、用车需求汇总草稿预览
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:`PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 的 809116 座位充足性校验,判据从「座位数 × 车辆数 < 该组最大单日乘车人数」改为「(座位数 − 1) × 车辆数 < 该组最大单日乘车人数」——每辆车扣 1 个司机座,与子订单级用车需求校验、fleet 单车派车(`AssignmentService`)口径统一。
|
||||
- 前端以前以为的(对应 `22_8152_...` changelog 描述的旧行为):座位数 × 车辆数刚好等于该组最大单日人数的分组能保存成功,例如 19 座 × 1 辆、最大日人数 19 能保存;同一响应体里 `remainingPassengerSeats`(余座回显)却是扣了司机座算出来的,可能已经是负数,前端只需原样展示负值提示缺口,不作为提交是否成功的依据。
|
||||
- 实际现在的行为:同样 19 座 × 1 辆、最大日人数 19 这组输入,现在直接被 809116 拒绝。「拦截口径」与「回显口径」现在共用同一份计算(`VehicleSeatCalculator.SeatSummary`),不再互相矛盾——凡是回显会算出 `remainingPassengerSeats` 为负的组合,保存时就先被拒绝,不会写入库。
|
||||
- 809116 报文占位符从 5 个变成 6 个,新模板与两个换算样例见「三、接口详情 → 1 → 业务边界」。
|
||||
- 本条**取代** `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md` 中关于 809116 判据与文案的描述,具体被取代的位置见「十、相关文档」。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
#8278 AC-1 定案:旧口径下「拦截不扣司机座、回显扣司机座」是对称子域的口径漂移(CODE_RULES §15.7)——子订单级容量校验与 fleet 单车派车早已扣司机座,只有团级拦截没扣,导致「刚好坐满」的组合能保存成功,但回显立刻显示余座为负。本单让团级拦截口径向已有的、更严格的口径看齐(扣司机座),而不是放松回显。
|
||||
|
||||
最强反例(评审已确认,留档):「刚好坐满、司机另开一辆车」这类特殊排法在新口径下会被拒绝——这是业务上刻意收紧,因为司机座不能卖给乘客,运营应当据此把车辆规格填成真实的乘客运力,而不是靠"整车都算乘客座"的旧口径蒙混过关。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 校验判据变更 + 错误报文格式变更 | 809116 扣司机座 |
|
||||
| 2 | 用车需求汇总草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 响应字段语义说明 | `violations[]` 里的 809116 条目共用同一份新判据/新报文 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是整份全量替换:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本条只改 809116 的判据与报文。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
| version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 |
|
||||
| remark | Body | String | ❌ | ≤500 | 整份需求备注 |
|
||||
| groups | Body | Array | ✅ | 可以是空数组(`@NotNull` 非 `@NotEmpty`) | 全部乘车分组;有在团需车户却零分组抛 809103 |
|
||||
| groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) |
|
||||
| groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group |
|
||||
| groups[].vehicleType | Body | String | ✅ | ≤64 | 车型文本/字典值 |
|
||||
| groups[].serviceStartDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本组服务开始日 |
|
||||
| groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 |
|
||||
| groups[].seats | Body | Integer | ❌ | `@Min(1)`;与 count 同填或同空 | 单车座位数(含驾驶位);**本次改动后参与扣司机座的容量判据**,存量分组可不传 |
|
||||
| groups[].count | Body | Integer | ❌ | `@Min(1)`;与 seats 同填或同空 | 车辆数量;只填一半抛 809118 |
|
||||
| groups[].specialTags | Body | Array<String> | ❌ | 取值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;含字典外编码整份拒绝(809117) |
|
||||
| groups[].remark | Body | String | ❌ | ≤500 | 该组备注 / 其他诉求 |
|
||||
| groups[].days | Body | Array | ✅ | 非空,且正好铺满本组服务日范围 | 逐日用车人数与成员 |
|
||||
| groups[].days[].tripDate | Body | LocalDate | ✅ | 落在本组服务日范围内、不重复、不缺日 | 越界或重复抛 809105,缺日抛 809106 |
|
||||
| groups[].days[].headcount | Body | Integer | ✅ | `@Min(1)`,且 ≥ 当日成员户数 | 该组该日**乘车人数**;本次改动后这个值与「(seats − 1) × count」比较,决定是否触发 809116 |
|
||||
| groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
|
||||
|
||||
#### 出参 `Result<GroupVehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
|
||||
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
|
||||
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
|
||||
| version | Integer | 版本号,下次提交须回传 |
|
||||
| remark | String | 整份备注 |
|
||||
| confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null |
|
||||
| planRefreshState / planRefreshReplayCount / blockedStage / planRefreshStalled / planRefreshStalledReason / planRefreshTimeoutAt / planRefreshReplayExhausted | - | 配车刷新状态相关字段,本次改动未涉及 |
|
||||
| groups | Array | 全部乘车分组;整团免车态为空数组 |
|
||||
| groups[].groupId | String | 分组主键(Long 序列化为字符串) |
|
||||
| groups[].groupCode | String | 分组键 = 车费 alloc_group |
|
||||
| groups[].vehicleType / vehicleTypeName | String | 车型文本/字典值 / 车型中文名 |
|
||||
| groups[].serviceStartDate / serviceEndDate | LocalDate | 本组服务日范围 |
|
||||
| groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null |
|
||||
| groups[].count | Integer | 车辆数量;存量分组为 null |
|
||||
| groups[].specialTags | Array<SpecialTagItem> | 特殊诉求标签(code + name) |
|
||||
| groups[].remark | String | 该组备注 |
|
||||
| groups[].totalSeatCount | Integer | 总座位数 = seats × count(含驾驶位);座位或数量缺一即为 null |
|
||||
| groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 |
|
||||
| **groups[].remainingPassengerSeats** | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;**可为负**;计算公式本次未变,但**新提交**里凡是会算出负值的组合,保存时已被 809116 拦在前面,不会写入库——负值目前只可能出现在改动前已保存、尚未被下一次提交重新校验的存量分组上 |
|
||||
| groups[].days[].tripDate / headcount / memberOrderIds / memberOrderCount | - | 逐日行程与成员,未变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": null,
|
||||
"remark": "#8278-AC6",
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": "#8278-AC6",
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": [2102749115559677953] },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": [2102749115559677953] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
真实网关实测响应(20 座 × 2 辆,headcount=38,`(20−1)×2=38` 恰好用完):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "2102750063359135746",
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"status": "DRAFT",
|
||||
"version": 1,
|
||||
"remark": "#8278-AC6",
|
||||
"confirmedBy": null,
|
||||
"confirmedAt": null,
|
||||
"planRefreshState": null,
|
||||
"planRefreshReplayCount": 0,
|
||||
"blockedStage": null,
|
||||
"planRefreshStalled": false,
|
||||
"planRefreshStalledReason": null,
|
||||
"planRefreshTimeoutAt": null,
|
||||
"planRefreshReplayExhausted": false,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": "2102750063363330049",
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"vehicleTypeName": "大巴系列",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": "#8278-AC6",
|
||||
"totalSeatCount": 40,
|
||||
"maxHeadcount": 38,
|
||||
"remainingPassengerSeats": 0,
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 整团没有在团需车户时,`groups: []` 是合法提交,响应 `groups` 为空数组;不受本次改动影响。
|
||||
- 存量分组(本次改动之前已保存的组)在数据库里原样保留旧值,不会因为本次上线被批量重算或清退;只有当它下次出现在某次 PUT 提交的 `groups` 数组里(哪怕自身字段一个没改,只是跟其他组一起整份提交)时,才会按新口径重新校验——如果它本身坐不下(扣司机座后不够),这次整份保存会被 809116 拒绝、全部字段零写入。
|
||||
- 车型中文名依赖车队侧车型库,降级行为不变:查不到编码或车队不可用时 `vehicleTypeName` 为 null,接口仍 200,不阻断页面。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
真实网关实测响应(20 座 × 2 辆,headcount=40,`(20−1)×2=38 < 40`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809116,
|
||||
"message": "第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 报文里「第 {0} 组」的 {0} 仍是 `groupCode`,不是序号(不变);一次只抛一条,按校验遍历顺序收集(不变)。
|
||||
- **新模板与占位符**:`第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人`。{1}=单车座位数(含司机座),{2}=车辆数,{3}=扣除的司机座数(恒等于 {2}),{4}=`(seats−1)×count` 算出的可载客座位,{5}=该组最大单日乘车人数。**占位符从旧模板的 5 个({0}~{4})变成 6 个({0}~{5}),其中两个位置的语义变了**:旧模板 `第 {0} 组座位数不足:{1} 座 × {2} 辆 = {3} 座,少于该组最大乘车人数 {4} 人` 里,{3} 是总座位数(`seats × count`),{4} 是该组最大乘车人数;新模板里 {3} 改为扣除的司机座数,{4} 改为可载客座位数,最大乘车人数挪到 {5}。{0}/{1}/{2} 语义不变。前端如果曾按位置/正则解析这句报文(而不是原样展示整句 `message`),必须同步改;如果只是原样 toast 整句 `message` 字符串,零改动即可。
|
||||
- 判据公式:`passengerSeatCapacity = max(0, seats × count − count)`,`passengerSeatCapacity < maxHeadcount` 时拒绝。两个换算样例(公式自算,均为「改前放行、改后拒绝」的收窄场景):
|
||||
- **19 座 × 1 辆、该组最大日人数 19**:`passengerSeatCapacity = max(0, 19×1 − 1) = 18`,`18 < 19` → 拒绝,`809116`:「第 {groupCode} 组座位数不足:19 座 × 1 辆,扣除 1 个司机座后可载客 18 人,少于该组最大乘车人数 19 人」。
|
||||
- **7 座 × 2 辆、该组最大日人数 13**:`passengerSeatCapacity = max(0, 7×2 − 2) = 12`,`12 < 13` → 拒绝,`809116`:「第 {groupCode} 组座位数不足:7 座 × 2 辆,扣除 2 个司机座后可载客 12 人,少于该组最大乘车人数 13 人」。
|
||||
- seats 与 count 同生同死规则不变:只填一个抛 809118(报文字面文本未变,仍是「第 {0} 组的座位数与车辆数必须同时填写,或同时留空」);两个都不填 = 存量形态,座位校验整体跳过。
|
||||
- 乐观锁 `version` 不一致抛 809102(不变);特殊诉求标签字典校验(809117)、逐字段约束均不变。
|
||||
|
||||
---
|
||||
|
||||
### 2. 用车需求汇总草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求 → 团级正式用车需求」为空/未确认时,前端用它拉一份系统按子订单在团情况自动配出的推荐草稿(`seats` 取组内最大单车座位、`count = ceil(最大日人数 / (seats − 1))`),供运营参考后再决定是否提交。本次改动不影响这个推荐公式本身(`GroupVehicleDraftAggregator.java:345`,已用 `git show a0973867f -- <该文件>` 核对,本次合并只改了该文件的 javadoc 注释,公式代码未动),只影响 `violations[]` 数组里 809116 条目的判据与报文——它与保存端点共用同一份校验函数(`collectFleetSpecViolations`)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
|
||||
| currentStatus | String | 当前正式需求状态;未建过正式需求为 null |
|
||||
| draft | Object | 结构同 `GroupVehicleRequirementSaveReqVO`;`version` 为当前生效版本号或 null;`draft.groups[].groupId` 恒为 null(草稿未落库) |
|
||||
| droppedFleetItems | Array | 因非主车型/车型字典不可用被剔除的子订单车队行;字段 orderId/orderNo/vehicleType/seats/count/keptVehicleType/reason |
|
||||
| staleHeadcountOrders | Array | 冻结人数与当前实际人数不一致的子订单;字段 orderId/orderNo/frozenHeadcount/liveHeadcount |
|
||||
| paddedOrderDays | Array | 被自动补天的子订单;字段 orderId/orderNo/dates |
|
||||
| violations | Array | 草稿自身触发的校验违规;恒非 null,无违规为空数组 |
|
||||
| violations[].code | Integer | 错误码,本次改动相关的是 809116 |
|
||||
| violations[].reason | String | 原因码,见「六.5」 |
|
||||
| violations[].detail | String | 渲染文案,与「三 → 1 → 错误响应」里同码条目的 `message` 逐字相同 |
|
||||
| violations[].groupCode / tripDate / orderId | String / LocalDate / String | 定位到具体分组/日期/子订单,均可为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"currentStatus": null,
|
||||
"draft": {
|
||||
"version": null,
|
||||
"remark": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": null,
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"] },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 整团没有可汇总内容时,`draft.groups` 为空数组,`droppedFleetItems`/`staleHeadcountOrders`/`paddedOrderDays`/`violations` 均为空数组(javadoc 原文:恒非 null)。
|
||||
- 有需车户缺少可汇总的行程用车需求时报 809121(本次改动未涉及此判据)。
|
||||
- 车型字典不可用时报 809120(本次改动未涉及此判据)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "violations": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 XXX 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:ORD001、ORD002",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 推荐公式 `count = ceil(最大日人数 / (seats − 1))` 保证草稿自己生成的分组,扣司机座后的可载客座位数恒 ≥ 该组最大日人数,所以由这个端点直接产出的草稿**正常情况下不会**在自己的 `violations[]` 里出现 809116。
|
||||
- `violations[]` 里若确实出现 809116 条目(例如运营手工改过草稿后再调这个端点做二次校验),其 `detail` 文案与占位符结构(6 段)与「三 → 1 → 错误响应」逐字相同,前端复用同一份渲染/解析逻辑即可,不需要为本端点单独适配。
|
||||
- 本端点只读,不落库,多次调用互不影响、无并发/幂等问题。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照(`PUT .../vehicle-requirement` 分组元素,`seats`/`count`/该组最大单日人数三者的关系)
|
||||
|
||||
| 场景 | seats × count | (seats−1) × count | 最大单日人数 | 判定 |
|
||||
|------|----------------|---------------------|----------------|------|
|
||||
| ✅ 明显够坐 | 35 | 34 | 20 | 放行(改前改后均放行) |
|
||||
| ✅ 恰好用完(边界) | 40 | 38 | 38 | 放行,`remainingPassengerSeats=0`(20 座×2 辆/38 人,已用真实网关实测) |
|
||||
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 19 | 18 | 19 | 809116 拒绝——**改前放行、改后拒绝** |
|
||||
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 14 | 12 | 13 | 809116 拒绝——**改前放行、改后拒绝**(7 座×2 辆/13 人) |
|
||||
| ❌ 两版本均拒绝 | 10 | 9 | 15 | 809116 拒绝(改前改后均拒绝,不扣司机座也不够坐) |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
`seats`/`count` 仍是「同填同空」的互斥对(不变):只提交其中一个会被 809118 拒绝;两个都不提交等价于存量形态,座位校验整体跳过。提交时不要依赖"隐藏输入框"的 UI 行为,后端只看 payload 里这两个字段是否同时为非 null。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
本次改动**不涉及表结构变化**,`groups.seats`/`groups.count`/`groups_day.headcount` 三列的落库口径与列值语义均未变——放行的提交,落库值仍是前端提交的原始 `seats`/`count`,后端不做任何扣减存储;变的只是"放不放行"这一步的判定发生在写库之前。
|
||||
|
||||
| 前端提交 | 改前落库结果 | 改后落库结果 |
|
||||
|----------|--------------|--------------|
|
||||
| 19 座 × 1 辆、最大日人数 19(该组) | 校验通过,`group.seats=19, group.count=1` 落库 | 809116 拒绝,**整份提交零写入**(不含该分组之外的其他改动) |
|
||||
| 20 座 × 2 辆、最大日人数 38(该组) | 校验通过,`group.seats=20, group.count=2` 落库 | 校验通过(边界恰好用完),落库同值 |
|
||||
|
||||
**零写入范围**:`PUT` 语义是整份全量替换,任一分组触发 809116 会导致这次提交整体失败,不只是该分组,其余分组在本次提交里的改动也不会落库(与改前逻辑一致,不是本次新增行为)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截,不变)。
|
||||
- `groupBatchId` 不存在 → 团期聚合层报错(不变,未在本次改动范围)。
|
||||
- 车队/字典服务降级 → 809120(`vehicleType` 字典不可用)判据与报文不变,仍与 809116 相互独立、互不影响。
|
||||
- 老数据兼容 → 存量分组的 `seats`/`count` 为 null 时 809116 判据整体跳过(不变);已有 `seats`/`count` 但未随本次改动重新保存的组,回显仍按旧值展示,可能带负的 `remainingPassengerSeats`(见「三 → 1 → 空数据 / 降级响应」)。
|
||||
- 生产环境 → 二期功能尚未在生产开放,本次改动的行为差异目前不会被任何生产流量触发。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
### status(正式用车需求状态,`GroupVehicleRequirementRespVO.status`)
|
||||
|
||||
**所属字段**: `GroupVehicleRequirementRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `DRAFT` | 草稿 | PUT 保存成功后的默认状态;本次改动后,坐不下的组合会在到达这一步之前就被 809116 拒绝 |
|
||||
| `CONFIRMED` | 已确认 | 整份确认后 |
|
||||
| `DISPATCHED` | 已派车 | fleet 已排车 |
|
||||
| `DONE` | 已完成 | 车务完成 |
|
||||
| `PENDING_RECONFIRM` | 待重新确认 | 确认后又被撤回/变更,需重新确认 |
|
||||
| `CANCELLED` | 已取消 | 整团取消 |
|
||||
|
||||
本次改动未涉及状态机迁移逻辑本身,取值与含义均不变。
|
||||
|
||||
### violations[].reason(`GroupVehicleAggregateDraftRespVO.Violation.reason`,本次改动相关的两个取值)
|
||||
|
||||
**所属字段**: `GroupVehicleAggregateDraftRespVO.Violation.reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `GROUP_SEATS_INSUFFICIENT` | 座位数不足 | 对应 809116;本次改动后判据改为扣司机座 |
|
||||
| `GROUP_SPEC_INCOMPLETE` | 车辆规格不完整 | 对应 809118;判据与报文字面文本均未变 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 809116 报文占位符数量 | 5 个({0}~{4}) | 6 个({0}~{5}):{3} 从「总座位数(`seats × count`)」改为「扣除的司机座数(=车辆数)」;{4} 从「最大乘车人数」改为「可载客座位数」;「最大乘车人数」后移到新增的 {5} |
|
||||
| 809116 判据表达式 | `seats × count < 该组最大单日乘车人数` | `max(0, seats × count − count) < 该组最大单日乘车人数`(即扣司机座后判断) |
|
||||
| `remainingPassengerSeats`(响应字段) | 计算公式含扣司机座,可能为负;负值组合仍能保存成功(拦截口径更宽松) | 计算公式不变;但**新提交**里会算出负值的组合,已先被 809116 拒绝,不会写入库 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 提交 `seats × count == 该组最大单日人数`(如 19 座×1 辆/19 人) | 放行 | 809116 拒绝 |
|
||||
| 提交 `(seats−1) × count == 该组最大单日人数`(如 20 座×2 辆/38 人) | 放行 | 放行(余座为 0,恰好用完) |
|
||||
| 提交 `(seats−1)×count < 该组最大单日人数 ≤ seats×count`(如 19×1/19、7×2/13) | 放行(不扣司机座时够坐) | **809116 拒绝**(扣司机座后不够坐,本次改动新增的拒绝区间) |
|
||||
| 拦截口径 vs 回显口径是否一致 | 刻意相差 1 个司机座/车,可能「保存成功但 remainingPassengerSeats 为负」 | 两口径共用同一份 `VehicleSeatCalculator` 计算,新提交不会再出现这种矛盾 |
|
||||
| 存量分组(改动前已保存、字段未变) | — | 不主动重算,原样保留在库里;只有它下次出现在某次 PUT 的 `groups` 数组里才会被新口径重新校验 |
|
||||
| fleet 单车派车(`AssignmentService`) | 已扣司机座 | 不变,本次改动是团级向它对齐 |
|
||||
| fleet 团级就绪检查黄牌(`GroupDispatchReadinessService#seatShortageWarnings`) | 不扣司机座 | 不变,仍不扣司机座,跟踪于 #8294,不在本次改动范围内 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 部分是——请求体/响应体字段结构未变,但同一份 payload 在「恰好等于旧口径边界、不足新口径边界」的场景下,会从改前的 200 变成改后的 809116(809116 是 HTTP 200 下的业务失败,不是传输层错误)。
|
||||
- **前端是否必须同步上线**: 视前端现有实现而定。若前端只是把 809116 的 `message` 整句原样 toast 展示,不改也能正常显示新文案,零改动。若前端曾按占位符位置/正则解析这句报文(例如截取「× {2} 辆」后面的数字单独展示),必须同步改成新的 6 段结构,否则会把 {3}(司机座数)或 {4}(可载客座位)错位显示。
|
||||
- **前端 workaround 清理点**: 若前端为「刚好坐满」这类输入写过专门的“应该能保存”预期用例,需要把预期改成会撞 809116;`remainingPassengerSeats` 为负仍是合法信号(不应做 `Math.max(0, x)` clamp,也不应据此拦截提交,这条 22_8152 的既有结论继续有效),但不能再假设"负值一定对应一个刚保存成功的新组合"——新提交里的负值组合已经在保存时被拒绝,负值目前只可能来自尚未被下一次保存重新校验的存量分组。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: `PUT .../vehicle-requirement` 的 809116 触发条件与报文;`GET .../vehicle-requirement/aggregate-draft` 响应体 `violations[]` 数组里 809116 条目的判据与报文(若出现)。
|
||||
- **零影响**:
|
||||
- 两个接口的请求体/响应体字段结构(无新增、无删除字段)。
|
||||
- 错误码码值本身(仍是 809116/809118,未变);809117(特殊诉求标签字典)、809102(乐观锁)、809103~809115、809119~809122 等其余错误码的判据与报文。
|
||||
- `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`(读取回显):不触发任何校验,原样返回库里已落值。
|
||||
- `GET .../requirement/vehicle-households`、`GET .../requirement-summary`、`GET .../requirement/confirm-check`:均未改动。
|
||||
- `aggregate-draft` 自身的推荐车辆数公式 `count = ceil(最大日人数 / (seats − 1))`(`GroupVehicleDraftAggregator.java:345`):本次合并只改了该文件的 javadoc,公式代码本身未动。
|
||||
- fleet 单车派车口径(`AssignmentService`):本就扣司机座,不受影响。
|
||||
- fleet 团级就绪检查黄牌口径(`GroupDispatchReadinessService#seatShortageWarnings`):仍不扣司机座,未随本次改动对齐,另开 #8294 跟进。
|
||||
- 生产环境:二期功能尚未在生产开放,本条改动不影响任何生产流量。
|
||||
- 历史数据:存量分组的 `seats`/`count`/`headcount` 不做批量重算或迁移,见「六.6」。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关实测(`https://api.test.1814.love`,hl-order-service-v3 dev-v3 @ `a0973867f`,2026-09-23 21:09:02 部署,`deploy-status` 读数 `BEHIND=0 STATE=ok`,两次 PUT 请求均晚于部署时刻):
|
||||
|
||||
```
|
||||
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
|
||||
20 座 × 2 辆,headcount=40(两天) → code=809116
|
||||
message=第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人 ✓
|
||||
GET 同一团期同一端点回读 → code=200, data=null(校验在写库前抛出,零写入)✓
|
||||
|
||||
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
|
||||
同一分组,headcount 改为 38(两天) → code=200, message=成功
|
||||
data.groups[0]: seats=20, count=2, totalSeatCount=40, maxHeadcount=38, remainingPassengerSeats=0 ✓
|
||||
GET 同一团期同一端点回读 → code=200, requirementId=2102750063359135746, status=DRAFT, version=1 ✓
|
||||
```
|
||||
|
||||
验证团期:`groupBatchId=2102749115823919105`(本轮自建夹具,未影响任何既有排期/团期)。两次 PUT 之间唯一变量是 `headcount`(40→38),`groupCode`/`vehicleType`/`seats`/`count`/日期范围完全相同,809116 与 200 的分野只能来自本次判据变更。
|
||||
|
||||
定向单测(`mvn -o -pl hl-order-service-v3 -am test -Dtest='GroupVehicleRequirementSaveTest*,GroupVehicleDraftAggregatorTest*,...'`,起跑 2026-09-23 20:31:52):17 个外层类 / 148 用例,Failures/Errors/Skipped 全 0,`BUILD SUCCESS`;含 `VehicleSeatCalculatorTest`(6)、`GroupVehicleRequirementSaveTest`(25,含新旧边界两条判据)、`GroupVehicleDraftAggregatorTest`(19)与 9 个 `@ArchTest` 载体。仅改注释的返工提交后,定向复跑受影响 3 类:50/0/0。
|
||||
|
||||
存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(`seats`/`count` 均非空)的分组 3 组;按新口径重算,这 3 组全部仍满足要求(新口径可载客座位 4~15 人不等,均 ≥ 该组最大日人数 2~5 人),按新口径会被拒的活跃分组数为 **0**。生产环境二期尚未开放,无生产存量。
|
||||
|
||||
限定:本次真实网关实测只覆盖了「一辆 20 座大巴 × 2 辆」这一种具体座位组合的团级 PUT 入口;其它座位数组合、子订单级校验、`aggregate-draft` 端点自身,由上述单测覆盖(`aggregate-draft` 与保存端点共享同一份校验代码,但本次未针对该端点单独发起真实 HTTP 调用)。存量核查的分母只有 3 组带规格(团级用车规格字段是 #8152 刚上线的新字段,前端入口尚未普及),0 组的读数只说明此刻不会新拒任何已存活跃版本,对未来接入更多分组后是否仍为 0 没有分辨力。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8278](https://git.1814.love/wx/HL/issues/8278)
|
||||
- 关联 PR: [wx/HL#8296](https://git.1814.love/wx/HL/pulls/8296)
|
||||
- 已知缺口(不在本次改动范围内): [wx/HL#8294](https://git.1814.love/wx/HL/issues/8294) —— fleet 团级就绪检查黄牌口径尚未对齐扣司机座
|
||||
- **订正声明(取代旧描述,不改动原文件)**:本条取代 `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md` 中以下位置关于 809116 判据与文案的描述:
|
||||
- 第 36-38 行「⚠️ 关键变化」块(「拦截口径不扣司机位」「回显口径扣司机位」「20 座 × 1 辆 / 20 人能保存成功,而返回的 `remainingPassengerSeats` 是 -1」):描述的是本次改动前的行为,改动后 20×1/20 这组输入已改为 809116 拒绝,两口径不再矛盾。
|
||||
- 第 40、42、44 行「展示侧」「提交侧」「响应体里没有任何字段表示…」三段:其中「是否允许保存由后端 809116 判定(口径:`seats × count` 与该组最大乘车人数比较,不扣司机位)」这句已过时,判据已改为扣司机位。
|
||||
- 第 240 行错误响应示例 `"第 BUS 组座位数不足:19 座 × 1 辆 = 19 座,少于该组最大乘车人数 20 人"`:这是旧模板(5 个占位符)的渲染结果,新模板见本条「三 → 1 → 错误响应」与「三 → 1 → 业务边界」。
|
||||
- 第 268 行业务边界「座位充足性判据 = `seats × count < 该组最大单日乘车人数`,不扣司机位」:判据已变,见本条「六.6」。
|
||||
- 第 354 行「20 座 × 1 辆载 20 人保存成功,余座 `-1`」:该结论已不成立,这组输入现在被拒绝。
|
||||
- 第 944 行行为级对比表格「提交坐不下的规格 | 接受(无此字段) | 809116 拒绝(判据不扣司机位)」一行,末列「判据不扣司机位」已过时。
|
||||
- 第 958-959 行「若此前把 `remainingPassengerSeats` 之类余座数做过 `Math.max(0, x)` 处理,必须撤掉」「不要新增『余座为负则禁止保存』的前端校验」:这两条建议本身**依然有效**,但其背景「后端放行的组合」已收窄——新提交里会被后端放行的组合,扣司机座后已经够坐,不会再出现负值;负值目前只可能来自尚未被下一次保存重新校验的存量分组。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8278](https://git.1814.love/wx/HL/issues/8278)
|
||||
- **PR**: [#8296](https://git.1814.love/wx/HL/pulls/8296)
|
||||
- **Merge commit**: [a0973867f](https://git.1814.love/wx/HL/commit/a0973867f)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend-groupbatch-vehicle-house-panel-gaps"
|
||||
title: "团期详情页「用车/用房」面板:乘车户全选汇总人数 + 确认按钮布局 + 对话入口缺失(3 项前端待办)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "d59180090e17c3fe1b9a1a4fb4fde22fc9f91685"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "wx 团期详情页用车/用房面板走查反馈,3 项均为纯前端待办:①乘车户全选+人数自动汇总(数据 participantCount 已在 orders 数组里,未被读取)②接送机逐户确认按钮布局(未右对齐)③子订单行缺与定制师对话入口(已有可用的既有后端会话接口 open-group,未接入)。三项互相独立,backend_status=not_required 是因为本文件不改任何后端契约。 | 2026-09-23 mmg 交付:三项全落(全选汇总/右对齐/两面板联系定制师透传 ChatDrawer)"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期详情页「用车/用房」面板:乘车户全选汇总人数 + 确认按钮布局 + 对话入口缺失(3 项前端待办)
|
||||
|
||||
> **服务**: 无后端服务变更(纯前端 UI;第 3 项引用的 `hl-user-service` `/admin/message/chat/open-group` 是已存在的能力,本次不新增/不修改)
|
||||
> **PR**: 无(前端尚未提交)
|
||||
> **Issue**: 无(wx 口头/会话反馈,未建 Gitea 工单)
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情页「用车」「用房」两个 Tab 面板
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
本文件是**待补齐清单**,不是"已交付变更"说明——三项各自独立、互不依赖,可分别排期,涉及文件都在 `hl-ui` `origin/v2.1` 分支。
|
||||
|
||||
---
|
||||
|
||||
## 一、乘车户「全选」+ 按所选户自动汇总人数
|
||||
|
||||
**涉及文件**: `src/views/order-v2/batch/detail/components/GroupVehicleRequirementEditModal.vue`(「正式用车需求编辑」弹窗,逐日行区块,第 165-201 行)
|
||||
|
||||
**现状(实测代码)**:
|
||||
- 「人数」是 `<n-input-number v-model:value="d.headcount">`(第 177-186 行),`headcountRule`(第 375-382 行)只校验"非空且 ≥1",没有联动逻辑。
|
||||
- 「乘车户」是 `<n-select multiple filterable :options="memberOptions">`(第 190-198 行),`memberIdsRule`(第 383-388 行)只校验"非空数组"。
|
||||
- 两个控件之间没有任何联动,也没有"全选"控件。
|
||||
|
||||
**可用但未被读取的数据**:`memberOptions`(第 323-333 行)目前只从 `props.orders` 取 `orderId`/`customerName`/`contactName`/`teamNo` 组装 `{ label, value }`,**没有带出 `participantCount`**。`participantCount` 其实已经在同一个 `orders` 数组里(团期订单列表 GB-ADM-003 契约字段),同级的 `VehicleHouseholdsSection.vue:71` 与 `RequirementTab.vue:476` 已经在直接读它展示"N 人"——不需要新接口,只是这个弹窗的 `memberOptions` computed 没把它带出来。
|
||||
|
||||
**需要的改动(供参考,不限定实现方式)**:
|
||||
1. 全选:一个按钮/开关把 `d.memberOrderIds` 置为 `memberOptions.value` 的全量 `value`。
|
||||
2. 自动汇总:`memberOptions` 补出 `participantCount`(或另建 `orderId → participantCount` 的 Map),选中集合变化时把 `d.headcount` 自动置为已选各户 `participantCount` 之和;`participantCount` 为 `null` 的户按 0 计。
|
||||
3. `headcount` 输入框保留手动可编辑——自动填充只是给个起点,若某户实际乘车人数与 `participantCount`(报名人数)不一致(如小孩不占位、分批乘车),管理员仍需手动改。
|
||||
|
||||
**业务边界**:
|
||||
- 这里自动算出的人数只是**填表辅助**,不是权威口径——整团/整日的用车人数汇总另有独立的后端工单在跟(wx/HL#8220,backend),本项做完之后 `d.headcount` 仍然只是这张表单自己的字段,不代表后端汇总接口上线后的口径一定与这里手填/自动填的值相同。
|
||||
- `participantCount` 为 `null` 的户参与"全选"时按 0 计入自动汇总,不阻断全选本身(该户仍会被选中,只是不贡献人数)。
|
||||
|
||||
---
|
||||
|
||||
## 二、接送机需求逐户「确认」按钮挪到行右侧
|
||||
|
||||
**涉及文件**: `src/views/order-v2/batch/detail/components/VehicleHouseholdsSection.vue`
|
||||
|
||||
**现状(实测代码 + 样式)**:
|
||||
- 「确认」按钮(`canConfirmReq(req)` 为真时渲染,第 122-129 行)与车型/状态标签、"XX 人"文字同处一行——`.vehicle-households__req-head`(第 92 行起)。
|
||||
- 该行样式(第 495-500 行):`display: flex; align-items: center; gap: 8px; flex-wrap: wrap;`,没有 `justify-content` 或对按钮单独设 `margin-left: auto`,按钮紧跟在"XX 人"文字后面,不在行的最右侧。
|
||||
|
||||
**需要的改动**:给 `.vehicle-households__req-head` 加 `justify-content: space-between`(把按钮包进一个独立的尾部容器),或直接给「确认」按钮加 `margin-left: auto`,把它推到行右侧。
|
||||
|
||||
**业务边界**:
|
||||
- 只影响 `.vehicle-households__req-head` 这一处的 TRANSFER 行确认按钮;用房面板 `RoomHouseholdsSection.vue` 全文没有等价的逐户「确认」按钮(已核查,仅第 127/289 行的注释提到"整团确认",属另一套交互,不在本项范围)。
|
||||
|
||||
---
|
||||
|
||||
## 三、用车/用房子订单行缺「和定制师发起对话」入口
|
||||
|
||||
**涉及文件**:
|
||||
- `VehicleHouseholdsSection.vue` 的 `.vehicle-households__card-head`(第 56-77 行)
|
||||
- `RoomHouseholdsSection.vue` 的 `.room-households__card-head`(第 44-68 行)
|
||||
|
||||
两处的 `v-for` 都已经在 `household` 上下文里拿到 `household.orderId`,但都没有任何对话/聊天入口(全文 grep「对话」「chat」「Chat」在这两个组件里零命中,`RoomHouseholdsSection.vue` 里唯一相关的是两处注释提及"整团确认",与聊天无关)。
|
||||
|
||||
**已就绪的后端契约(本次核实,非新增,backend_status=not_required 是因为接口本身没有变化,只是首次要接给这两个面板用)**:
|
||||
|
||||
`POST /admin/message/chat/open-group`(`hl-user-service` `ChatMessageController.openGroup`,工单 #7211)
|
||||
|
||||
- 请求体 `ChatOpenGroupReqVO`:`{ "orderId": "70123" }`——`orderId` 必填(`@NotNull`),团期子订单 id,雪花值按字符串传(与本仓其它 19 位 ID 字段一致,避免 JS 精度丢失)。
|
||||
- 响应体 `ChatOpenFullRespVO`(继承 `ChatOpenRespVO`):`conversationKey`(如 `GROUP:70123`,不含 adminId 对)、`peerAdminId`、`peerName`、`peerRole`、`peerRoleLabel`、`peerOnline`、`unreadCount`、`isNew`、`order`(订单卡)、`thread`(首屏 20 条消息)、`unreadTotal`(合并未读,本次调用会把该会话标记已读之后的值)。
|
||||
- 团期管理员是团队制,不指派到人:定制师这一侧打开会话时,对端固定是「团期管理员」团队占位——`peerRole="GROUP_ADMIN"`、`peerRoleLabel="团期管理员"`,不是某一个具体的管理员账号。
|
||||
- 准入:该订单当前定制师(CUSTOMIZER),或当前角色持权限码 `group-batch:demand:confirm`(超管天然通过);其余角色统一拒绝且零写入。
|
||||
- 错误码:缺 `orderId` → **281012**「缺少订单上下文」;不满足上述准入身份 → **281002**「无权访问该会话」;该订单不是团期子订单(团期摘要 `groupBatchId` 为空)→ **281015**「该订单不是团期子订单,无法联系团期管理员」。
|
||||
- 打开会话之后,同一个 `conversationKey`(`GROUP:{orderId}`)继续走通用的两个端点,不区分模块:`GET /admin/message/chat/{conversationKey}/messages`(上滑翻页取更早历史)、`POST /admin/message/chat/{conversationKey}/messages`(发消息)。如果车务/房务面板已有复用的聊天组件,直接换 `open` 端点为 `open-group` 即可接入,不需要重新对接翻页/发送逻辑。
|
||||
- 网关:`hl-user-service` 的 `/admin/message/**` 路由已覆盖,不需要新配路由。
|
||||
|
||||
**业务边界**:
|
||||
- 用车、用房两个面板的子订单行请求体字段完全一样(都只传 `orderId`),可以共用同一套"发起对话"组件。
|
||||
- 会话按 `GROUP:{orderId}` 维度建:同一个子订单如果在用车、用房两个面板里都出现,两边打开的是同一条会话,不会产生重复会话或未读数分叉。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台团期详情页「用车」「用房」两个 Tab 组件(模板/脚本/样式,纯前端文件)。
|
||||
- **零影响**:
|
||||
- 本文件三项都不改后端契约;第三项引用的 `POST /admin/message/chat/open-group` 是首次被这两个面板消费,接口本身(CUSTOMIZER↔团期管理员团队场景)未做任何变更。
|
||||
- 其它已在用 `open-group`/`open-fleet`/`open-house` 的既有调用方不受影响。
|
||||
- 团期用车需求的后端保存/校验接口(`GroupVehicleRequirementSaveReqVO` 等)不受影响——第一项只改前端表单填充逻辑,提交给后端的字段结构不变。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- `open-group` 契约来源:`hl-user-service/src/main/java/com/hulalv/user/notification/chat/controller/ChatMessageController.java`(工单 #7211)。
|
||||
- `participantCount` 字段来源:`GB-ADM-003` 团期订单列表契约(`src/api/orderV2GroupBatch.js` 第 143-160 行注释)。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- 无 PR / 无 Issue(本文件为纯前端待办清单,未建 Gitea 工单)。
|
||||
|
||||
### 联系人
|
||||
|
||||
- 第三项接口如有疑问:@wx
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "frontend"
|
||||
title: "查看需求 Tab:整团级操作收进顶部操作条 + 子订单行新增「需求详情」弹窗"
|
||||
consumer: admin
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f713b1c65c061a18754c5bc759d1c50c3dd5eb3e"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "2026-09-23 wx 对「查看需求」Tab 提三点:①「新增正式行程用车需求」按钮应该在顶部操作条这里;②该按钮独立成一张卡片操作不方便;③要一个按钮点开弹窗看用房/用车需求详情;并总结「这个 Tab 操作整体不方便,整理优化下」。本条是给 mmg 的 UI 重排规格。后端零改动:四项改动用到的端点与字段全部已在 dev-v3 上线并被本 Tab 现有代码调用,无新增端点、无出入参变化、无网关路由变化、无 DDL。字段清单逐项对 origin/dev-v3 的 VO 源码核过(GroupHotelHouseholdsRespVO / GroupVehicleHouseholdsRespVO / GroupBatchRoomPlanDetailRespVO)。 | 2026-09-23 mmg 交付:五区块卡头全撤,顶部操作条(刷新一个顶六个/新增编辑正式需求平铺,整团级缺失时 primary ghost/撤回免车受控重开收更多下拉,判定经 ops-state 上抛);预检条目可点(户级滚动定位名单行+高亮,车侧整团级直开编辑弹窗);名单表加需求详情弹窗(两 tab 只读复用 households 快照零新请求,分房段按需拉 room-plans 过滤本户);五卡自绘折叠分区默认只展开两汇总(n-collapse 懒挂载不满足 eager fetch);scroll-x 钉死列宽合计 1568;顺带修 A 段遗留 doConfirm stale refs ReferenceError;7 spec 99 例全绿"
|
||||
updated_at: "2026-09-23"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 查看需求 Tab:操作重排 + 逐户需求详情弹窗
|
||||
|
||||
> **服务**: hl-order-service-v3(**后端零改动**)
|
||||
> **页面**: 管理后台 → 团期订单 → 团期详情 → 「查看需求」Tab
|
||||
> **文件**: `src/views/order-v2/batch/detail/components/RequirementTab.vue` 及其子组件
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 仅前台渲染与交互编排;无端点、无出入参、无网关路由、无 DDL 变化
|
||||
|
||||
---
|
||||
|
||||
## 一、现状与问题(对 `origin/v2.1` = `61a549bd` 实读)
|
||||
|
||||
这个 Tab 现在是「一条操作条 + 一张子订单表 + 五张平铺卡片」的结构:
|
||||
|
||||
| 位置 | 承载的整团级操作 |
|
||||
|---|---|
|
||||
| `RequirementTab.vue:29-51` `.requirement-tab__ops` | 刷新预检 / 打回选中户(N) / 整团确认需求 |
|
||||
| `GroupVehicleRequirementSection.vue` 的 `#header-extra` | 新增(编辑)正式行程用车需求 / 整份撤回·撤回免车 / 声明整团免车 / 受控重开配车窗口 |
|
||||
| 五张卡片各自的卡头 | 各自一个「刷新」 |
|
||||
|
||||
三个具体问题:
|
||||
|
||||
1. **整团级操作散在三处**,其中一处还在第四屏。
|
||||
2. **阻断项和解除阻断的按钮离得最远**。顶部预检提示条会打出「团期 xxx 尚未形成正式用车需求」,而消除它的那个按钮在下方「正式用车需求」卡片的卡头里——看见问题的地方和能动手的地方隔了四张卡片。
|
||||
3. **一户的需求被劈在两张卡片里**。「用房 · 子订单订房记录」和「用车 · 子订单需求记录」各自按户列一遍,要看清某一户到底报了什么,得在两张卡片之间上下翻。子订单表里的那一行才是「这一户」,但那行点不开。
|
||||
|
||||
---
|
||||
|
||||
## 二、要做的四件事
|
||||
|
||||
### 1. 整团级操作全部收进顶部操作条
|
||||
|
||||
`.requirement-tab__ops` 改为(从左到右):
|
||||
|
||||
```
|
||||
[刷新] [打回选中户(N)] [新增正式行程用车需求] [更多 ▾] ………… [整团确认需求]
|
||||
```
|
||||
|
||||
- **`新增正式行程用车需求` 平铺**(wx 明确指定放这里)。文案沿用现有逻辑:有需求时显示「编辑正式需求」,无需求时显示「新增正式行程用车需求」。
|
||||
- **`更多 ▾` 收三个低频且互斥的**:`整份撤回` / `撤回免车`(二者 `v-if` / `v-else-if` 互斥)、`声明整团免车`、`受控重开配车窗口`。收进下拉的理由是这四个按钮的出现与否随状态变化,平铺会让操作条宽度随状态跳动;下拉让操作条形状恒定。
|
||||
- **按钮层级**:`整团确认需求` 保持 `type="primary"`,是这个页面的终点。`新增正式行程用车需求` 用默认样式,**但当预检提示条里出现「尚未形成正式用车需求」这一条时,把它切成 `type="primary" ghost`** —— 此刻它才是「现在该点的那个」。
|
||||
- **卡片保留,只清空卡头**。`GroupVehicleRequirementSection` 这张卡片继续展示正式用车需求的内容(车队、座位、状态),只把 `#header-extra` 里那四个按钮搬走。
|
||||
|
||||
实现上父子已经通着:`RequirementTab.vue:155-168` 已持有 `ref="vehicleReqRef"`,把 `canEdit` / `isWaived` / `canWithdraw` / `canReopen` 四个 computed 与四个动作的入口方法 `defineExpose` 出来即可,不需要新增 props 或提升状态。
|
||||
|
||||
### 2. 预检提示条的每一条做成可点
|
||||
|
||||
这是「操作不方便」最直接的来源,优先级最高。
|
||||
|
||||
| 提示条里的条目 | 点击后 |
|
||||
|---|---|
|
||||
| `HL2026xxxx 定制师张三 — 未提交房需求` | 滚动到子订单表对应行并高亮(`orderId` 已在 missing 项里) |
|
||||
| `团期 21008xxxx 尚未形成正式用车需求` | 直接打开 `GroupVehicleRequirementEditModal`,即操作条上那个「新增正式行程用车需求」 |
|
||||
|
||||
把「看见问题」和「动手解决」接在一起,用户就不必自己在页面里找对应入口。
|
||||
|
||||
### 3. 子订单表操作列新增「需求详情」→ 弹窗
|
||||
|
||||
在现有「进入」旁边加一个「需求详情」。点开一个 `n-modal`(或右侧 `n-drawer`),**两个 Tab:用房 / 用车**,只读。
|
||||
|
||||
**关键点:这个弹窗的价值是「按户聚合」,不是「多给字段」。**
|
||||
两张列表卡片其实已经把下面这些字段渲染出来了(`RoomHouseholdsSection.vue` / `VehicleHouseholdsSection.vue` 实读确认),弹窗做的是把同一户散在两处的内容收到一屏。
|
||||
|
||||
#### 用房 Tab
|
||||
|
||||
数据来自本 Tab 已加载的 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households`,按 `orderId` 过滤,**不发新请求**。
|
||||
|
||||
- 头部:`customerName` / `teamNo` / `orderNo` / `participantCount` / `consultantName`
|
||||
- 状态:`statusName`,外加 `countedInSummary` 的显式标识(计入汇总 / 未计入汇总)
|
||||
- 配房需求:`remark`
|
||||
- 特殊标签:`specialTags[]`
|
||||
- 打回留痕:`returnRemark` +`returnedAt`(有值才显示)
|
||||
- 逐晚表,来自 `days[]`:
|
||||
|
||||
| 列 | 取值 | 既有约定(沿用,勿改) |
|
||||
|---|---|---|
|
||||
| 第 N 天 | `dayNumber` | |
|
||||
| 入住日 | `stayDate` | |
|
||||
| 自行预订 | `customerSelfBooked` | 为 true 时该晚不该被当成缺口 |
|
||||
| 酒店 | `hotels[].hotelName` | 为 null 显「未知酒店」,**行不隐藏**(间数仍有效) |
|
||||
| 地点 | `hotels[].district` | **用 `district` 不用 `city`**,契约实测 `city` 几乎恒为同一地级市,区分不出地点 |
|
||||
| 房型 | `rooms[].roomTypeName` | 回落链:`roomTypeName` → `roomCategoryName` → `roomCategory` → 「未知房型」 |
|
||||
| 间数 | `rooms[].roomCount` | |
|
||||
|
||||
#### 用车 Tab
|
||||
|
||||
数据来自 `GET .../requirement/vehicle-households`,同样按 `orderId` 过滤复用已加载数据。逐条渲染 `requirements[]`:
|
||||
|
||||
- `kindName`(行程用车 / 接送机)+ `statusName`
|
||||
- `serviceDates[]`
|
||||
- `headcount` / `totalSeatCount`(含驾驶位)/ `remainingPassengerSeats`(负数标「缺口」,沿用现有红色样式)
|
||||
- `fleet[]` 车队明细
|
||||
- `pickupRequired` / `dropoffRequired` —— **仅 TRANSFER 有意义,TRAVEL 恒为 null**,TRAVEL 下整行不渲染
|
||||
- `specialTags[]` / `remark`
|
||||
- `returnRemark` + `returnedAt`(有值才显示)
|
||||
|
||||
#### 可选第二段:这一户「已经配成什么样」
|
||||
|
||||
如果要让弹窗从「他报了什么」延伸到「我们给他配了什么」,`GET /v3/admin/order/group-batch/{groupBatchId}/room-plans`(H12 整团配房明细,只读、不含金额)已经有分房层,按 `orderId` 过滤即可:
|
||||
|
||||
```
|
||||
days[].plans[].allocations[] → orderId / orderNo / roomCount / roomGroupNo(家庭分组 F{N}) / 入住人数 / allocSource(AUTO|MANUAL)
|
||||
```
|
||||
|
||||
外层还带 `hotelName` / `roomTypeName` / `planStatus`(PENDING|CONFIRMED)/ `replaceReason`。这是弹窗里唯一需要多发一个请求的部分,按需加载即可。
|
||||
|
||||
### 4. 刷新收敛成一个,明细卡片默认折叠
|
||||
|
||||
- 顶部操作条那个 `刷新预检` 改名 `刷新`,一次刷预检 + 五个区块;**撤掉五张卡片各自的刷新按钮**。用户心智里这个页面只有一个「刷新」。
|
||||
- 五张卡片(用房·汇总 / 用房·子订单订房记录 / 用车·汇总 / 正式用车需求 / 用车·子订单需求记录)改 `n-collapse`,**默认只展开两张「汇总」**。明细按需展开,首屏留给操作条、预检提示条和子订单表。
|
||||
|
||||
---
|
||||
|
||||
## 三、数据来源汇总(零新接口)
|
||||
|
||||
| 用途 | 端点 | 本 Tab 现状 |
|
||||
|---|---|---|
|
||||
| 子订单表 | `GET /v3/admin/order/group-batch/{id}/orders` | 已调 |
|
||||
| 预检 | `GET .../requirement/confirm-check` | 已调 |
|
||||
| 用房逐户(弹窗用房 Tab) | `GET .../requirement/hotel-households` | 已调,字段够 |
|
||||
| 用车逐户(弹窗用车 Tab) | `GET .../requirement/vehicle-households` | 已调,字段够 |
|
||||
| 整团确认 / 打回 | `POST .../requirement/confirm`、`.../requirement/reject` | 已调 |
|
||||
| 正式用车需求 读/存/撤回/免车/重开 | `GET`·`PUT .../vehicle-requirement`、`.../withdraw`、`.../waive`、`.../reopen` | 已调 |
|
||||
| 弹窗「已配成什么样」(可选段) | `GET .../room-plans` | 本 Tab 未调,端点已上线(H12) |
|
||||
|
||||
---
|
||||
|
||||
## 四、业务边界
|
||||
|
||||
- **本条只覆盖「查看需求」Tab**。「配房明细」Tab(`RoomPlansTab.vue`)的结构不在本次重排范围内。
|
||||
- **需求侧的房间粒度只到「房型 × 间数」**。`GroupHotelHouseholdsRespVO` 的最细一层是 `HouseholdRoomItem{roomTypeId, roomTypeName, roomCategory, roomCategoryName, roomCount}`,弹窗的用房 Tab 给不出「哪一间」。
|
||||
- **「谁住哪一间」这层数据全系统没有**。配房侧能到「哪一户的哪个家庭分组 F{N}、几个人、住哪家酒店哪个房型的几间」(上面第二段那个可选来源),但**出行人姓名与房间的绑定不存在**——`GET .../room-plans` 的接口说明里明写了不返回出行人姓名与联系方式。要做到姓名级,是一次真正的新增能力,不是取数问题。
|
||||
- **用车侧被打回的户可能在弹窗里无行可渲染**。`VehicleHouseholdsSection.vue:210` 的注释写着「打回行失活不在列表(与用房侧『列出打回户灰显』刻意不同)」。【需确认】这一条我只读到注释、没有实测接口返回,实现时以 `vehicle-households` 的实际返回为准;若确实不返,弹窗用车 Tab 对被打回的户要给一个明确空态文案,不要显示成「这户没报用车需求」。
|
||||
- **`受控重开配车窗口` 进下拉后仍受既有约束**:DISPATCHED 定稿后要重配须先开窗拿令牌,PENDING_RECONFIRM 下续开/取当前令牌同走这个入口(后端幂等返回既有令牌,`809203` 兜他人窗口)。挪位置不改变这套行为。
|
||||
- **`整团免车` 是合法逃生口不是异常态**。顶部那个 `本团整团免车` 的 `n-tag`(`checkResult?.vehicleWaived === true`)要保留在操作条附近,免车时车侧校验整体跳过,别让它看起来像出错了。
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8231"
|
||||
title: "物资 tab 的写入口仍按 MATERIAL_PREPARING 前置隐藏——后端已放开到出行前四态,前端不改则运营点不到入口"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "e15dd73bd36a1d27374f9206797243f8a5e4326c"
|
||||
target_release: ""
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "#8231 后端已把物资三个写接口(新增行 / 改数量 / 软删行)的阶段门从单一 MATERIAL_PREPARING 放宽到出行前四态(RECRUITING / RESOURCE_PREPARING / MATERIAL_PREPARING / PENDING_DEPARTURE),2026-09-23 已部署 TEST 并四态逐一实测通过。但 SuppliesPanel.vue 里有一个 isMaterialPreparing 计算属性把手工录入按钮、操作列表头、数量行内编辑、删除按钮四处一并前置隐藏,非 MATERIAL_PREPARING 阶段物资 tab 仍是一张没有操作列的只读表,运营点不到任何入口——后端这次放开在界面上等于没发生。需要把这四处的判据从「等于 MATERIAL_PREPARING」改成「在出行前四态之内」。注意「确认物资」按钮(同文件 28 行)走的是另一个接口 confirm-material,#8231 没有放开它,那一处的阶段门要原样保留、不要跟着一起改。配导游 / 配摄影侧前端零改动:ChipItemsPanel.vue 的配置按钮条件只有 v-if=configRole,不看 batchStatus,后端放开后即刻生效。 | 2026-09-23 mmg 交付:canConfigureSupplies 四态管三写入口,isMaterialPreparing 保留仅管确认物资"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期物资 tab: 写入口的阶段门需从单一阶段放宽到出行前四态(管理后台)
|
||||
|
||||
> **仓库 / 分支**: `mmg/hl-ui` @ `v2.1`
|
||||
> **文件**: `src/views/order-v2/batch/detail/components/SuppliesPanel.vue`
|
||||
> **Issue**: #8231
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 团期详情页「物资」tab 的操作列与三个写入口
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
后端已经放开,**前端不改则本次改动在界面上等于没发生**。
|
||||
|
||||
`RESOURCE_PREPARING`(资源准备中)是最典型的一态:团期要集齐四项 ready(房 / 车 / 导 / 摄)才推得进
|
||||
`MATERIAL_PREPARING`,所以今天运营抱怨「物资配不了」,多数时候团期正停在 `RESOURCE_PREPARING`。
|
||||
后端这次就是为这个场景放开的,而前端在这一态下连操作列都不渲染。
|
||||
|
||||
---
|
||||
|
||||
## 一、后端现状(已部署 TEST 并实测)
|
||||
|
||||
| 接口 | 方法 | 路径 | 改后可用状态 |
|
||||
|---|---|---|---|
|
||||
| 新增备品行 | POST | `/v3/admin/order/group-batch/{groupBatchId}/supplies` | 出行前四态 |
|
||||
| 调整数量 | PUT | `/v3/admin/order/group-batch/supplies/{batchSuppliesId}/quantity` | 出行前四态 |
|
||||
| 软删备品行 | DELETE | `/v3/admin/order/group-batch/supplies/{batchSuppliesId}` | 出行前四态 |
|
||||
|
||||
出行前四态 = `RECRUITING` / `RESOURCE_PREPARING` / `MATERIAL_PREPARING` / `PENDING_DEPARTURE`。
|
||||
|
||||
出行后(`TRAVELLING` / `TRIP_FINISHED` / `REVIEWING` / `SETTLED`)与 `CANCELLED` 仍拒,
|
||||
**错误码从 589520 换成了 589598**,文案「出行后不可再配置导游 / 摄影 / 物资」。
|
||||
|
||||
TEST 实测(2026-09-23,`dev-v3` @ `fe752f16c`):四态各跑「增 → 改数量 → 删」三端点共 12 次调用全 200;
|
||||
三个出行后态各返 589598 且零写入。
|
||||
|
||||
---
|
||||
|
||||
## 二、前端需要改的四处(行号按 `origin/v2.1` @ `f713b1c6`)
|
||||
|
||||
| # | 行 | 现状 | 期望 |
|
||||
|---|---|---|---|
|
||||
| 1 | 16 | `v-if="isMaterialPreparing"` —— 「手工录入」按钮 | 出行前四态可见 |
|
||||
| 2 | 52 | `<span v-if="isMaterialPreparing">操作</span>` —— 操作列表头 | 出行前四态可见 |
|
||||
| 3 | 81 | `v-if="isMaterialPreparing"` —— 数量 `n-input-number` 行内编辑(否则走 `v-else` 的只读纯文本) | 出行前四态可编辑 |
|
||||
| 4 | 94 | `<span v-if="isMaterialPreparing">` —— 整个删除按钮单元格 | 出行前四态可见 |
|
||||
|
||||
判据单源在 179 行:
|
||||
|
||||
```js
|
||||
// #7528 阶段门:仅物料准备中可见三个写操作与确认入口(其余状态后端仍守,前端前置隐藏)
|
||||
const isMaterialPreparing = computed(() => props.detail?.batchStatus === 'MATERIAL_PREPARING')
|
||||
```
|
||||
|
||||
连带两处会自动跟着变,确认改后表现正常即可:
|
||||
|
||||
- 183 行 `gridColumns`:操作列出现与否决定列宽是 6 列还是 5 列
|
||||
- 188 行 `emptyText`:空态文案分「可从备品库选择或手工录入」与「本期尚未配置物资」两种
|
||||
|
||||
---
|
||||
|
||||
## 三、⚠️ 不要一起改的两处
|
||||
|
||||
| 位置 | 原因 |
|
||||
|---|---|
|
||||
| 28 行「确认物资」按钮 | 它调的是 `POST /v3/admin/order/group-batch/{groupBatchId}/confirm-material`,属七项硬门准入链路,**#8231 没有放开它**。那一处的 `MATERIAL_PREPARING` 判据要原样保留 |
|
||||
| 4 行「从备品库选择」按钮 | 它本来就没挂阶段门、一直可见。改前它在非 `MATERIAL_PREPARING` 下点了会被 589520 拒,改后四态内可正常落库——**不需要改代码,但回归时要覆盖它** |
|
||||
|
||||
因为 28 行与那四处不再同一个判据,建议把 179 行拆成两个计算属性(例如「可配置窗口」与「可确认物资」),
|
||||
而不是把现有的 `isMaterialPreparing` 就地改语义——后者会把确认物资一起放开,那是另一件事。
|
||||
|
||||
---
|
||||
|
||||
## 四、配导游 / 配摄影侧:前端零改动
|
||||
|
||||
`ChipItemsPanel.vue` 的配置按钮条件只有 `v-if="configRole"`,不看 `batchStatus`;
|
||||
弹窗只有 `:disabled="!rosterReady"`(名单加载态)。招募中按钮本来就渲染、点得下去,
|
||||
只是改前后端回 589552 被拦截器透成 toast。后端放开后该路径即刻可用,无需前端配合。
|
||||
|
||||
---
|
||||
|
||||
## 五、错误文案
|
||||
|
||||
按 589520 做分支的地方需要改认 **589598**。589520 未退役(保留占位防号段复用),但物资三口不再抛它。
|
||||
|
||||
---
|
||||
|
||||
## 六、回归建议
|
||||
|
||||
在 `RESOURCE_PREPARING` 的团期上验:操作列出现 → 手工录入一行 → 改数量 → 删除 → 从备品库选择再加一行。
|
||||
再在 `REVIEWING` 或 `TRIP_FINISHED` 的团期上验:操作列不出现,且即便直接打接口也返 589598。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw(#8231,PR #8234 / #8237,已部署 TEST)
|
||||
- 前端:mmg
|
||||
- 同批后端条目:`23_8231_配导游配摄影物资放开到出行前四态-修改接口-管理后台.md`
|
||||
- 前例:`17_frontend_团期物资清单补改数量删除与手工录入-前端优化-管理后台.md`
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "frontend"
|
||||
title: "「用房 · 汇总」首屏必现「汇总加载失败」:同页两个组件调同一个接口,后发的把先发的 abort 了"
|
||||
consumer: admin
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "4b33ccbd9af619723a9c58373531627cb6e31e9b"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "2026-09-23 wx 反馈团期详情「查看需求」Tab 的「用房 · 汇总」显示「汇总加载失败,可点右上角「刷新」重试」,同页「用房 · 子订单订房记录」「用车 · 汇总」正常。根因在 hl-ui:RoomSummarySection 与 VehicleSummarySection 在同一 tick 各调一次 getGroupRequirementSummary(同 method+url+params+data),而该封装未传 cancelDuplicate:false,request.js 的去重拦截器用后发的 AbortController abort 掉先发的那条;渲染顺序 Room 在前,所以被取消的恒是 Room,catch{} 把 cancel 当失败吞掉、summary 留 null、落到空态文案。后端与数据均正常:测试服 nginx 09-21~09-23 该团期 16 次 requirement-summary 全部 HTTP 200、响应体与应用日志确认成功的载荷等长,order-v3 应用日志 09-23 两次打出「全团需求汇总完成 inGroupOrders=2 hotelNeeded=2 hotelCounted=1」,09-22 18:55 起零 ERROR;DB 侧两户 needs_hotel=1、一户有 active 需求行,与页面「共 2 户 · 计入 1 户」吻合。本条无后端改动。 | 2026-09-23 mmg 交付:requirement-summary 收归 RequirementTab 统一取数只调一次,两汇总区块改 props 下发(失败态只看 error prop),catch 护栏区分 ERR_CANCELED(去重 abort/路由切换)不落假失败态,空 id/序号守卫随取数上移父级;3 spec 31 例锁单次取数/取消护栏/失败态"
|
||||
updated_at: "2026-09-23"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 「用房 · 汇总」首屏必现加载失败:请求去重把它自己的请求取消了
|
||||
|
||||
> **服务**: hl-order-service-v3(**后端零改动,后端与数据均正常**)
|
||||
> **页面**: 管理后台 → 团期订单 → 团期详情 → 「查看需求」Tab → 「用房 · 汇总」区块
|
||||
> **接口**: `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 仅前端。所有团期、所有用户、每次首次进入该 Tab 必现;点「刷新」即恢复
|
||||
|
||||
---
|
||||
|
||||
## 一、现象
|
||||
|
||||
「用房 · 汇总」渲染成空态:`汇总加载失败,可点右上角「刷新」重试`。
|
||||
同一页的「用房 · 子订单订房记录」「用车 · 汇总」「用车 · 子订单需求记录」全部正常。
|
||||
点该区块右上角「刷新」立刻就好。
|
||||
|
||||
---
|
||||
|
||||
## 二、根因(对 `origin/v2.1` = `61a549bd` 源码实读)
|
||||
|
||||
三个事实叠起来构成这个缺陷:
|
||||
|
||||
**① 同一个接口被同页两个组件各调一次,且在同一 tick**
|
||||
|
||||
| 文件 | 行 | 调用 |
|
||||
|---|---|---|
|
||||
| `src/views/order-v2/batch/detail/components/RoomSummarySection.vue` | `:74` `:169` | `import { getGroupRequirementSummary }` → `await getGroupRequirementSummary(batchId)` |
|
||||
| `src/views/order-v2/batch/detail/components/VehicleSummarySection.vue` | `:94` `:152` | 同上 |
|
||||
|
||||
两者都在 `watch([active, groupBatchId], …, { immediate: true })` 里触发(`RoomSummarySection.vue:180-190`、`VehicleSummarySection.vue:163-173`),`active` 同源,必定同 tick。
|
||||
`RequirementTab.vue` 的渲染顺序是 `RoomSummarySection` 在前、`VehicleSummarySection` 在后。
|
||||
|
||||
**② 这个 API 封装没关掉去重**
|
||||
|
||||
`src/api/orderV2GroupBatch.js:563-568`:
|
||||
|
||||
```js
|
||||
export function getGroupRequirementSummary(groupBatchId, config = {}) {
|
||||
return http.get(`${BASE}/${String(groupBatchId)}/requirement-summary`, null, {
|
||||
...V3,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
同一个文件里 `confirm`(`:507`)、`reject`(`:528`) 以及另外十余个封装都带着 `cancelDuplicate: false`,**只有这个没带**。
|
||||
|
||||
**③ 去重拦截器对同 key 的后发请求,是 abort 掉先发的那条**
|
||||
|
||||
`src/utils/request.js:260-275`:
|
||||
|
||||
```js
|
||||
if (config.cancelDuplicate === false) { …不入 pending 列表… }
|
||||
…
|
||||
oldController.abort('取消重复请求')
|
||||
const controller = new AbortController()
|
||||
```
|
||||
|
||||
key 的构成(`:235-237`)是 `[method, url, stableStringify(params), stableStringify(data)].join('&')`。
|
||||
本接口无 query、无 body,两个组件算出的 key **逐字符相同**。
|
||||
|
||||
**⇒ 后发的 Vehicle 把先发的 Room abort 了。** Room 那条在 adapter 发出前就被取消,所以网络层根本看不到它(测试服 nginx 每次页面加载只落**一条** `requirement-summary`,与此吻合)。
|
||||
|
||||
**④ 取消被当成了失败**
|
||||
|
||||
`RoomSummarySection.vue:169-175`:
|
||||
|
||||
```js
|
||||
try { const res = await getGroupRequirementSummary(batchId); … } catch { }
|
||||
```
|
||||
|
||||
裸 `catch {}` 不区分 axios cancel 与真实失败,`summary` 留 `null`,模板落到 `:55` 的空态文案。
|
||||
点「刷新」时只有一条请求在途、没人取消它,所以必然成功——这就是「刷新即好」的由来。
|
||||
|
||||
---
|
||||
|
||||
## 三、后端与数据侧的核查结果(均正常,供排除用)
|
||||
|
||||
- **nginx 访问日志**(测试服 `access.log{,.1,.2.gz}`):该团期 09-21~09-23 共 **16 次** `requirement-summary`,**全部 HTTP 200**;响应体 1116 B(09-22 13:04 上 `transferSummary` 之前)/ 1358 B(之后)。对照组:前端在路由参数未就绪时发的空 id 请求 `group-batch//requirement-summary` 只有 132 B,说明 1358 B 是完整成功载荷、不是 `code≠0` 的 `Result`。
|
||||
- **order-v3 应用日志**:`2026-09-23 09:09:45.822` / `09:11:01.395` 两次 `全团需求汇总完成 groupBatchId=2100856430494973953 inGroupOrders=2 hotelReqs=1 hotelNeeded=2 hotelCounted=1 vehicleReqs=1 transferReqs=1`;09-22 18:55 起 order-v3 零 ERROR。
|
||||
- **数据**:两个子订单 `needs_hotel=1`;`HL20260918155619496` 有 `is_active=1` 的 `PENDING_REVIEW` 用房需求(两晚、JSON 合法、`special_tags=["禁烟"]`),`HL20260922161158291` 无用房需求行(`flow_status=AWAITING_PROFILE`)。与页面「共 2 户 · 计入汇总 1 户」逐项吻合,无脏数据。
|
||||
- **服务端代码**:`GroupBatchRequirementService.summary()` 链路上 `filterCountedHotelRequirements` 对无需求行的户只是不入集合(不会 NPE),`aggregateDailyRooms` / `parseServiceDates` / `aggregateVehicleSeats` / `collectSpecialTags` 均 try/catch 降级,酒店名与车型名的 Feign/字典加载失败一律降级为空 Map。无 null-guard 缺口。
|
||||
|
||||
---
|
||||
|
||||
## 四、修复建议
|
||||
|
||||
**主修(结构)**:由 `RequirementTab` 只请求一次 `requirement-summary`,经 props 下发给 `RoomSummarySection` / `VehicleSummarySection`。父组件已持有两者的 ref,改动面可控;这样同一接口一次页面加载只打一次,顺带省掉一次重复请求。
|
||||
|
||||
**护栏(更重要,建议与主修一起做)**:两个 Summary 组件的 `catch {}` 要区分「被取消」与「真失败」,被取消时不要落成「加载失败」态:
|
||||
|
||||
```js
|
||||
} catch (e) {
|
||||
if (axios.isCancel?.(e) || e?.code === 'ERR_CANCELED') return // 保持 loading / 旧数据,不置失败态
|
||||
// 真实失败才走原逻辑
|
||||
}
|
||||
```
|
||||
|
||||
`src/utils/request.js:596` 已经有这个判别写法可以直接抄。之所以说它比主修更重要:`request.js:315` 在**路由切换**时也会 `abort('路由切换,取消请求')`,同样会落进这个 `catch`;而且只要将来任何组件再复用同一个接口,缺陷就会原样复现。这条护栏把「一次修好」变成「不会再犯」。
|
||||
|
||||
**一行止血(如果要先快速恢复)**:给 `getGroupRequirementSummary` 的配置加上 `cancelDuplicate: false`,与同文件其余封装写法一致。代价是同一页会真的发两次相同请求。
|
||||
|
||||
### ⚠️ 一个容易踩空的点
|
||||
|
||||
`request.js` 里有**两个名字相近、行为不同**的开关,别改错:
|
||||
|
||||
| 开关 | 位置 | 对本缺陷 |
|
||||
|---|---|---|
|
||||
| `dedupe`(时间窗内重复请求直接拒) | `:173-208` | **对 GET 默认就不生效**——`:183` 写着 `methodUpper === 'GET' && config.dedupe !== true` 才进这段。设 `dedupe: false` 改变不了任何事 |
|
||||
| `cancelDuplicate`(同 key 后发 abort 先发) | `:260-275` | **就是它**,对 GET 生效 |
|
||||
|
||||
---
|
||||
|
||||
## 五、业务边界
|
||||
|
||||
- **不限于某一个团期**。机制是同 tick 双请求、后者必取消前者,与团期数据无关;09-22 14:37 团期 `2101506167098511362` 的访问日志同样只落一条 `requirement-summary`。
|
||||
- **只影响「用房 · 汇总」区块的首屏**。「用车 · 汇总」拿得到数据(它是后发的那条),其余三个区块调的是别的接口,不受影响。点「刷新」或整团确认后的 `reload()` 都能恢复。
|
||||
- **09-22 白天的 order-v3 应用日志已被滚动删除**(只保留 3 份),那个窗口 13 次请求的「后端成功」由 nginx 200 + 响应体长度(与 09-23 已被应用日志直接证实成功的载荷等长)支撑,不是应用层直接证据;09-23 的两次有应用日志直接证实。
|
||||
- **前端侧的最初取证来自测试服实际部署的构建产物**(`/var/www/hl-admin/assets/`,构建于 09-23 09:59),本条正文里的文件行号是事后对 `origin/v2.1` = `61a549bd` 源码复核后给出的,两侧一致。
|
||||
- **顺带一个非本单的观察**:进入团期详情页时,页面在路由参数就绪前会发三条空 id 请求——`group-batch//requirement-summary`、`group-batch//vehicle-requirement`、`group-batch//requirement/hotel-households`(各返 132 B 业务错误)。来源不是 `RoomSummarySection`(它有 `!groupBatchId` 守卫)。对功能无影响,列在这里供一并排查。
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7804"
|
||||
title: "定时任务健康自检"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "后端已部署 TEST 并经网关实测;运维排查接口,管理后台无需接入页面"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 定时任务健康自检(#7804)
|
||||
|
||||
> **服务**: hl-user-service(经网关调用,无需关心服务端口)
|
||||
> **PR**: #8337
|
||||
> **Issue**: #7804
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 定时任务运维排查(超级管理员),无前端页面
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
新增只读端点,逐个定时任务并列返回三方状态:**期望状态**、**`sys_job.status`(意图)**、**Quartz 触发器实况**。三方不一致的条目 `healthy=false`,并附告警原文。
|
||||
|
||||
**三条会直接影响你怎么用的点**:
|
||||
|
||||
1. 🔴 **判断任务是否真在跑,要看 `quartzState`,不能看 `dbStatus`**。`sys_job.status` 只是意图:用裸 SQL 把它置 PAUSED,任务照跑(TEST 上 1044 就这样连跑了 4 天)。原有的 `GET /admin/job` 列表只有 `sys_job.status`,不能用来判断启停。
|
||||
2. **只有 SUPER_ADMIN 能调**;其他角色返回业务码 `210301`。
|
||||
3. **成功码是 `code: 200`**;业务失败也返回 HTTP 200,**一律按 `body.code` 判**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
`SysJobService.initScheduledJobs()` 为了集群安全是纯增量的,只补注册 ACTIVE 任务,从不移除已有的 Quartz 触发器。所以「裸 SQL 置 PAUSED」停不掉任务,而原来的 `[JobHealth]` 自检只读 `sys_job`,这种漂移下自检是绿的。本单把自检改为以 Quartz 为准,并把结果开放成接口。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 定时任务健康自检 | GET | `/admin/job/health` | 新增接口 | 只读,逐个任务返回期望 / sys_job / Quartz 三方状态与告警 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 定时任务健康自检 `GET /admin/job/health`
|
||||
|
||||
**VO**: `SysJobHealthVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
运维或超级管理员排查「某个定时任务到底有没有在跑」「sys_job 与调度器是否一致」时调用。结果与服务日志里的 `[JobHealth]` WARN 同源同文:启动时和每天 09:30 自动跑一次,日志里只记不一致的条目;本接口返回全部条目,健康的也在内。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `Authorization` | Header | String | ✅ | `Bearer <token>` | 管理端 token,角色须为 SUPER_ADMIN |
|
||||
|
||||
#### 出参
|
||||
|
||||
`Result<List<SysJobHealthVO>>`,按 jobId 升序排列。另有两类条目追加在末尾:名单内但 sys_job 里不存在的 job,以及 Quartz 里有调度而 sys_job 没有这一行的 job。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `jobId` | Long | 任务 ID。🔴 部分任务是 19 位雪花 ID(如 `2067459972995731458`),前端按字符串处理 |
|
||||
| `jobName` | String / null | 任务名;sys_job 中不存在该行时为 null |
|
||||
| `expectedStatus` | String / null | 期望状态,来自配置 `hl.job.health-check.expected-status`;未列入名单的任务为 null。取值 `ACTIVE` / `PAUSED` |
|
||||
| `dbStatus` | String / null | `sys_job.status`,即意图;sys_job 中不存在该行时为 null。取值 `ACTIVE` / `PAUSED` |
|
||||
| `quartzState` | String | Quartz 触发器实况,取值见「六.5 枚举」 |
|
||||
| `healthy` | Boolean | 期望、sys_job、Quartz 三方一致时为 true |
|
||||
| `message` | String / null | 告警原文;健康时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/job/health
|
||||
Authorization: Bearer <SUPER_ADMIN token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"jobId": 1039,
|
||||
"jobName": "Fleet导入临时文件收敛",
|
||||
"expectedStatus": null,
|
||||
"dbStatus": "PAUSED",
|
||||
"quartzState": "NORMAL",
|
||||
"healthy": false,
|
||||
"message": "jobId=1039 name=Fleet导入临时文件收敛 期望=未列入名单 DB=PAUSED Quartz=NORMAL:sys_job 与 Quartz 漂移(sys_job 只是意图,Quartz 才是实况)"
|
||||
},
|
||||
{
|
||||
"jobId": 1041,
|
||||
"jobName": "团期出发推进",
|
||||
"expectedStatus": "ACTIVE",
|
||||
"dbStatus": "ACTIVE",
|
||||
"quartzState": "NORMAL",
|
||||
"healthy": true,
|
||||
"message": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**示例说明**:两条取自测试环境 2026-09-24 14:54 的真实调用(当时 1039 是人为构造的漂移),实际返回全部任务(TEST 上 34 条)。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
sys_job 为空且 Quartz 无孤儿调度时,`data` 为空数组:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": [] }
|
||||
```
|
||||
|
||||
读某个任务的 Quartz 状态失败时不降级为「健康」,而是返回一条 `healthy=false`、`quartzState=UNKNOWN`、`message` 含「Quartz 状态读取失败」的条目;孤儿扫描失败时同样返回一条「Quartz 孤儿触发器扫描失败」。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**非超级管理员**:
|
||||
|
||||
```json
|
||||
{ "code": 210301, "message": "仅超级管理员可管理定时任务", "data": null, "success": false }
|
||||
```
|
||||
|
||||
**未带 token**:
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "缺少有效的 Authorization 头", "data": null, "success": false }
|
||||
```
|
||||
|
||||
查 sys_job 表失败时不会返回「全绿」,而是走全局异常处理返回错误码。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读,不改 sys_job,也不改 Quartz。
|
||||
- **全部** sys_job 行都会比对 sys_job 与 Quartz,不只期望名单内的。
|
||||
- 名单内的任务额外比对期望值:期望 ≠ Quartz 实况、期望 ≠ sys_job 都会报。
|
||||
- Quartz 实况折算口径:`NORMAL` / `BLOCKED` 会继续触发,算 ACTIVE;`PAUSED` / `COMPLETE` / `NONE` 不会再触发,算 PAUSED;`ERROR` / `UNKNOWN` 一律不健康。
|
||||
- `POST /admin/job/{id}/trigger` 在任务未注册时建的一次性调度(`trigger_<id>`)不算孤儿。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误请求对照
|
||||
|
||||
| 做法 | 结论 |
|
||||
|---|---|
|
||||
| ✅ 用 `quartzState` / `healthy` 判断任务是否真在跑 | 权威源 |
|
||||
| ❌ 用 `GET /admin/job` 列表里的 `status` 判断启停 | 那只是 sys_job 意图,会与实况永久漂移 |
|
||||
| ✅ 停任务走 `POST /admin/job/{id}/pause` | 会同时摘掉 Quartz 触发器 |
|
||||
| ❌ 裸 SQL `UPDATE sys_job SET status='PAUSED'`(+ 重启) | 触发器保留,任务照跑;本接口会报「漂移」 |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 期望名单为空时仍会比对全部任务的 sys_job 与 Quartz 一致性。
|
||||
- 配置 `hl.job.health-check.enabled=false` 只关闭启动时和每日的自检日志,不影响本接口。
|
||||
- 经网关落到 primary / secondary 任一实例,结果相同:Quartz 是 JDBC 集群,状态读的是共享的 `QRTZ_*` 表。
|
||||
|
||||
---
|
||||
|
||||
## 六.5 枚举 / 数据字典
|
||||
|
||||
### quartzState
|
||||
|
||||
**所属字段**: `quartzState` | **类型**: `String`
|
||||
|
||||
| 值 | 含义 | 折算 |
|
||||
|---|---|---|
|
||||
| `NORMAL` | 调度中(库里显示 WAITING / ACQUIRED / EXECUTING) | ACTIVE |
|
||||
| `BLOCKED` | 上一次执行尚未结束 | ACTIVE |
|
||||
| `PAUSED` | 已暂停 | PAUSED |
|
||||
| `COMPLETE` | 不再触发 | PAUSED |
|
||||
| `NONE` | 未注册 | PAUSED |
|
||||
| `ERROR` | 触发器出错 | 不健康 |
|
||||
| `UNKNOWN` | 读取失败 | 不健康 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 既有 `/admin/job/**` 端点(增删改查、pause / resume / trigger / logs)的入参、出参、行为均不变。
|
||||
- `initScheduledJobs()` 的启动注册逻辑不变,仍然只增不减,不会自动摘除漂移的触发器。
|
||||
- 无数据库结构变更,无网关路由变更(沿用 `Path=/admin/job/**`)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关调用(`https://api.test.1814.love`),hl-user-service 已部署 `dev-v3 @ 1ffd758db`(本单合并提交,两个实例在 14:38 完成滚动重启)。token 是自签的(adminId=1002 / test_admin),经 SSH 临时写入 Redis 令牌位,窗口为 14:54:10–14:54:13,用完即还原。
|
||||
|
||||
```
|
||||
构造:UPDATE sys_job SET status='PAUSED' WHERE job_id=1039 → sys_job=PAUSED,QRTZ_TRIGGERS 仍 WAITING
|
||||
修复前代码重启 → [JobHealth] 关键定时任务状态自检通过,共核 5 项 (阴性对照:旧版测不出)
|
||||
修复后代码重启 → [JobHealth] jobId=1039 … DB=PAUSED Quartz=NORMAL:sys_job 与 Quartz 漂移 ✓
|
||||
GET /admin/job/health(SUPER_ADMIN)→ 200,34 条中仅 1039 healthy=false;1041/1042 healthy=true ✓
|
||||
还原 1039 为 ACTIVE 后再调 → 200,34 条全部 healthy=true ✓
|
||||
role=ADMIN → code 210301「仅超级管理员可管理定时任务」 ✓
|
||||
不带 token → code 401「缺少有效的 Authorization 头」 ✓
|
||||
```
|
||||
|
||||
本地单测:`mvn -o -pl hl-user-service -am test` 共 5051 例,0 失败 / 0 错误;另做 4 个变异体验证,每个都被测试杀死。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #7538 / #7640:引入 `[JobHealth]` 自检
|
||||
- #8268:用 Flyway 下线 1044,同时清理了 `QRTZ_*`
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7804](https://git.1814.love/wx/HL/issues/7804)
|
||||
- 关联 PR: [wx/HL#8337](https://git.1814.love/wx/HL/pulls/8337)
|
||||
- 运维须知: `hl-user-service/docs/deployment.md`(定时任务启停)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7804](https://git.1814.love/wx/HL/issues/7804)
|
||||
- **PR**: [#8337](https://git.1814.love/wx/HL/pulls/8337)
|
||||
- **Merge commit**: [1ffd758db](https://git.1814.love/wx/HL/commit/1ffd758db)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,710 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8219"
|
||||
title: "团级正式用车需求:有户能交没交时保存被拒(809123),交不了的户列为豁免户"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "9eb8268fd04f55821a3200b342c18e8cafcc7768"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "新增错误码 809123;三个接口新增豁免户清单字段;809122 / 809121 不再报豁免户 | 2026-09-24 mmg 交付:编辑弹窗诊断区五数组→六数组新增「以下户未参与排车(豁免)」块(订单号+reasonName),保存成功提示点名豁免户数;RequirementTab 预检区新增豁免户展示块(不进 ready 不置灰);三接口 JSDoc 补 809123/豁免字段口径;workaround 清理点 grep 零命中;Section 45 例+RequirementTab 33 例全绿"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团级正式用车需求——未提交户阻断保存、豁免户显式列出
|
||||
|
||||
> **存放目录**: `changelogs-v2/{YYYY-MM}/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3 (端口 8007)
|
||||
> **PR**: #8317、#8319
|
||||
> **Issue**: #8219
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「团期详情 → 用车 Tab」的正式用车需求编辑弹窗(保存、自动汇总)与整团确认预检横幅
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **保存正式用车需求多一个拒绝码 809123**:团里有户「本可以提交行程用车需求却没提交」时,`PUT .../vehicle-requirement` 直接拒绝,报文逐户列出团号(无团号回落订单号)。改前保存完全不看户侧有没有提交,而 809109 又要求每个需车户逐日被分组覆盖,运营只能把没提交的户也编进分组、替它编车型人数才能存下去。
|
||||
2. **三个接口新增豁免户清单**:`exemptHouseholds`(保存响应、自动汇总响应)/ `vehicleExemptHouseholds`(整团确认预检)。豁免户是「此刻在系统里提交不了行程用车需求」的户,它们**不阻断**保存 / 汇总 / 确认,也**不要求被分组覆盖**,但会逐户连同原因列出来,前端需要展示。
|
||||
3. **809122(预检 / 整团确认)与 809121(自动汇总)不再报豁免户**:三处判定同源,豁免户只出现在豁免户清单里,不再被当成「未提交」。
|
||||
4. **809109 只对已提交户判覆盖**:没有行程用车需求的户(无论未提交还是豁免)都不再被要求编进分组。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期详情页用车 Tab 上,管理员保存团级正式行程用车需求时,系统不检查各户有没有提交行程用车需求;同时 809109 要求每个需车户的出发~返回日都被某个分组覆盖。结果是有户没提交时,管理员为了存下去只能把那户也编进分组、替它填车型人数,这份编出来的数据随后会下发给车务。
|
||||
|
||||
本次把「户有没有提交」收成一份判定,三类户分别处理:
|
||||
|
||||
| 户的类别 | 判定 | 保存(PUT) | 整团确认预检 | 自动汇总 |
|
||||
|------|------|------|------|------|
|
||||
| 已提交 | 有 active 行程用车需求 | 照常参与 809109 覆盖 | 照常 | 照常进草稿 |
|
||||
| 未提交 | 订单定制中,且(团期未冻结 或 该户最新行程用车需求被打回) | **809123 拒绝** | 809122 | 809121 |
|
||||
| 豁免 | 订单不在定制中;或团期已过资源准备、该户未被打回 | 不阻断,列入 `exemptHouseholds` | 不报 809122,列入 `vehicleExemptHouseholds` | 不报 809121、不进草稿,列入 `exemptHouseholds` |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 新增错误码 + 出参新增字段 | 新增 809123;响应新增 `exemptHouseholds` |
|
||||
| 2 | GB-ADM-012 整团确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 出参新增字段 + 取值口径变更 | 新增 `vehicleExemptHouseholds`;809122 不再报豁免户 |
|
||||
| 3 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 出参新增字段 + 取值口径变更 | 新增 `exemptHouseholds`;809121 不再报豁免户、豁免户不进草稿 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
用车 Tab「编辑正式用车需求」弹窗点保存时调用,全量替换整份团级正式用车需求(主表备注 + 全部乘车分组 + 全部逐日行)。本次新增:团里存在「本可以提交却没提交」行程用车需求的户时,保存被 809123 拒绝;提交不了的户(豁免户)不阻断保存,在响应 `exemptHouseholds` 里列出。鉴权走 `group-batch:demand:confirm`,仅「资源准备中」阶段可保存。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID |
|
||||
| version | Body | Integer | ❌ | 首次保存传 `null`;之后必须回传上次响应的 `version` | 乐观锁版本号 |
|
||||
| remark | Body | String | ❌ | — | 整份备注 |
|
||||
| groups | Body | Array | ✅ | 不能为 `null`;空数组合法(有需车户时会被 809103 拒) | 全部乘车分组 |
|
||||
| groups[].groupId | Body | Long | ❌ | 沿用已有分组时必须回传 | 分组主键 |
|
||||
| groups[].groupCode | Body | String | ✅ | 非空;同一份内不重复;已有分组不得改名 | 分组键 |
|
||||
| groups[].vehicleType | Body | String | ✅ | 非空;须在车队车型字典内 | 车型 |
|
||||
| groups[].serviceStartDate | Body | String | ✅ | `yyyy-MM-dd` | 本组服务开始日 |
|
||||
| groups[].serviceEndDate | Body | String | ✅ | `yyyy-MM-dd`,不早于开始日 | 本组服务结束日 |
|
||||
| groups[].seats | Body | Integer | ❌ | 与 `count` 同填同空 | 单车座位数 |
|
||||
| groups[].count | Body | Integer | ❌ | 与 `seats` 同填同空 | 车辆数 |
|
||||
| groups[].specialTags | Body | Array | ❌ | 字典 `vehicle_special_demand` 编码 | 特殊诉求标签 |
|
||||
| groups[].remark | Body | String | ❌ | — | 分组备注 |
|
||||
| groups[].days | Body | Array | ✅ | 非空;须正好铺满本组服务日范围 | 逐日用车明细 |
|
||||
| groups[].days[].tripDate | Body | String | ✅ | `yyyy-MM-dd` | 行程日 |
|
||||
| groups[].days[].headcount | Body | Integer | ✅ | 不小于当日成员户数 | 当日用车人数 |
|
||||
| groups[].days[].memberOrderIds | Body | Array | ✅ | 非空;须全属本团在团户 | 当日乘车子订单 ID |
|
||||
|
||||
#### 出参 `Result<GroupVehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.requirementId | String | 正式需求主键 |
|
||||
| data.groupBatchId | String | 团期聚合主键 |
|
||||
| data.status | String | 保存后恒为 `DRAFT` |
|
||||
| data.version | Integer | 新版本号,下次保存回传 |
|
||||
| data.remark | String | 整份备注 |
|
||||
| data.confirmedBy | String | DRAFT 时为 `null` |
|
||||
| data.confirmedAt | String | DRAFT 时为 `null` |
|
||||
| data.groups[] | Array | 保存后的全部乘车分组(字段与改前一致) |
|
||||
| data.exemptHouseholds[] | Array | **本次新增**:本次保存时被豁免的在团需车户,按 orderId 升序;无豁免户时为空数组 `[]` |
|
||||
| data.exemptHouseholds[].orderId | String | 子订单 ID |
|
||||
| data.exemptHouseholds[].orderNo | String | 子订单号 |
|
||||
| data.exemptHouseholds[].reason | String | 豁免原因码;本接口只会出现 `ORDER_NOT_CUSTOMIZING`,见六.5 |
|
||||
| data.exemptHouseholds[].reasonName | String | 豁免原因中文名(后端下发) |
|
||||
|
||||
`GroupVehicleRequirementRespVO` 的其余字段(配车刷新观测块 `planRefreshState` 等)与改前一致。**`exemptHouseholds` 只在保存响应里填充**;同一个 VO 在读接口(`GET .../vehicle-requirement`)、撤回、免车、重开的响应里该字段为 `null`,表示「这个响应不回答豁免问题」。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
TEST 实测往返(团期 `2103018513226752001`,三户:两户已提交、一户待支付未提交):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": null,
|
||||
"remark": "#8219 AC-5 成功用例(ii)",
|
||||
"groups": [
|
||||
{
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2026-12-11",
|
||||
"serviceEndDate": "2026-12-13",
|
||||
"days": [
|
||||
{ "tripDate": "2026-12-11", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] },
|
||||
{ "tripDate": "2026-12-12", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] },
|
||||
{ "tripDate": "2026-12-13", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
同一次请求的真实响应(配车刷新观测块字段省略,与改前一致):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "2103018859139407874",
|
||||
"groupBatchId": "2103018513226752001",
|
||||
"status": "DRAFT",
|
||||
"version": 1,
|
||||
"remark": "#8219 AC-5 成功用例(ii)",
|
||||
"confirmedBy": null,
|
||||
"confirmedAt": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": "2103018859143602177",
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"vehicleTypeName": "大巴系列",
|
||||
"serviceStartDate": "2026-12-11",
|
||||
"serviceEndDate": "2026-12-13",
|
||||
"days": [
|
||||
{ "tripDate": "2026-12-11", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 },
|
||||
{ "tripDate": "2026-12-12", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 },
|
||||
{ "tripDate": "2026-12-13", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"exemptHouseholds": [
|
||||
{
|
||||
"orderId": "2103018517358141442",
|
||||
"orderNo": "HL20260924150741326",
|
||||
"reason": "ORDER_NOT_CUSTOMIZING",
|
||||
"reasonName": "订单不在定制中"
|
||||
}
|
||||
]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
没有豁免户时 `exemptHouseholds` 是空数组,不是 `null`(TEST 实测团期 `2103018935689650178`,两户都已提交);其余结构同上:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2103018935689650178",
|
||||
"status": "DRAFT",
|
||||
"version": 1,
|
||||
"exemptHouseholds": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团里有户本可以提交却没提交行程用车需求(本次新增,测试环境实测原文):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809123,
|
||||
"message": "团期「第30期 T29 #7441 AC-29 batch6」有 2 户尚未提交行程用车需求,暂不能保存正式用车需求:26-0674、26-7797",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
需车户存在但一个分组都没提交(改前已有):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809103,
|
||||
"message": "本团存在需要用车的子订单,至少要提交一个乘车分组",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|------|------|
|
||||
| 809123 | **新增**:存在未提交户;报文列出户数与逐户团号(无团号回落订单号),不含 orderId |
|
||||
| 809103 | 需车户非空但 `groups` 为空数组 |
|
||||
| 809101 / 809102 / 809104~809110 / 809115 | 与改前一致 |
|
||||
| 589501 | 团期阶段不是「资源准备中」 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判序:阶段守卫 → 免车态守卫(809115)→ 源状态守卫(809101)→ 乐观锁(809102)→ 分组改名守卫(809104)→ **未提交户阻断(809123)** → 六条逐日校验(809103~809110)。809123 排在逐日校验之前:有户没交时先让运营去催定制师,而不是先看到某户某天没被覆盖。
|
||||
- 809123 与 809103 ~ 809110 一样是整笔零写入:拒绝时库里的正式需求版本不变。
|
||||
- 本接口只在「资源准备中」可调,这个阶段定制师都能提交需求,所以本接口的豁免户只有「订单不在定制中」一种,`exemptHouseholds[].reason` 只会是 `ORDER_NOT_CUSTOMIZING`。
|
||||
- 豁免户不要求被任何分组覆盖(809109 只对已提交户判),也可以被编进分组(不会报错)。
|
||||
- 全团需车户都是豁免户时,`groups` 为空数组仍报 809103;分组的 `memberOrderIds` 又不能为空,这种团按现行口径要么把豁免户编进分组,要么走整团免车(`POST .../vehicle-requirement/waive`)。
|
||||
- 809123 报文里只列团号(团号为空时列订单号),不带雪花 orderId,与 809121 / 809112 / 809114 同一口径;前端展示报文即可,不要从报文里解析订单号。
|
||||
|
||||
---
|
||||
|
||||
### 2. GB-ADM-012 整团确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
|
||||
|
||||
**VO**: `Path 参数 → GroupBatchRequirementCheckRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台点「整体确认需求」之前调用,把按钮置灰并摊开缺失清单。它与 `POST .../requirement/confirm` 共用同一套校验。本次新增 `vehicleExemptHouseholds`:列出提交不了行程用车需求、因而不报 809122 的户;809122 只报「本可以提交却没提交」的户。鉴权走 `group-batch:demand:confirm`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body |
|
||||
|
||||
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.groupBatchId | String | 团期聚合主键 |
|
||||
| data.batchStatus | String | 团期当前状态码 |
|
||||
| data.ready | Boolean | 可整体确认:`missing` 空且 `vehicleMissing` 空且阶段可确认。豁免户不影响 `ready` |
|
||||
| data.missing[] | Array | 住宿缺失清单(与改前一致) |
|
||||
| data.vehicleWaived | Boolean | 整团免车时为 `true` |
|
||||
| data.vehicleMissing[] | Array | 车侧缺失清单(结构与改前一致) |
|
||||
| data.vehicleMissing[].reason | String | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)**本次起只报未提交户,不再报豁免户** |
|
||||
| data.vehicleMissing[].orderId | String | 涉及的子订单 ID;团级条目为 `null` |
|
||||
| data.vehicleMissing[].orderNo | String | 子订单号;团级条目为 `null` |
|
||||
| data.vehicleMissing[].detail | String | 人话报文 |
|
||||
| data.vehicleExemptHouseholds[] | Array | **本次新增**:车侧豁免户,按 orderId 升序;`vehicleWaived=true` 或无豁免户时为空数组 `[]` |
|
||||
| data.vehicleExemptHouseholds[].orderId | String | 子订单 ID |
|
||||
| data.vehicleExemptHouseholds[].orderNo | String | 子订单号 |
|
||||
| data.vehicleExemptHouseholds[].reason | String | `ORDER_NOT_CUSTOMIZING` 或 `REQUIREMENT_FROZEN`,见六.5 |
|
||||
| data.vehicleExemptHouseholds[].reasonName | String | 豁免原因中文名 |
|
||||
| data.groupVehicleRequirementId | String | 活跃正式用车需求主键;无则 `null` |
|
||||
| data.groupVehicleRequirementStatus | String | 活跃正式用车需求状态;无则 `null` |
|
||||
| data.groupVehicleRequirementVersion | Integer | 活跃正式用车需求版本号;无则 `null` |
|
||||
|
||||
其余字段(`batchStatusName`、`checkedResourceTypes`、`transferSubmitEnabled`、`transferDeclaredWithoutRequirement` 等)与改前一致。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2099927193172815873/requirement/confirm-check HTTP/1.1
|
||||
Host: <网关地址>
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2099927193172815873",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"ready": false,
|
||||
"missing": [],
|
||||
"vehicleWaived": false,
|
||||
"vehicleMissing": [
|
||||
{
|
||||
"reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": "2099927193143455746",
|
||||
"orderNo": "HL20260916022352215",
|
||||
"detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务"
|
||||
},
|
||||
{
|
||||
"reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": "2099927202878435329",
|
||||
"orderNo": "HL20260916022354533",
|
||||
"detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务"
|
||||
}
|
||||
],
|
||||
"vehicleExemptHouseholds": [
|
||||
{
|
||||
"orderId": "2099927222981734402",
|
||||
"orderNo": "HL20260916022359336",
|
||||
"reason": "ORDER_NOT_CUSTOMIZING",
|
||||
"reasonName": "订单不在定制中"
|
||||
},
|
||||
{
|
||||
"orderId": "2099927233643655170",
|
||||
"orderNo": "HL20260916022401707",
|
||||
"reason": "ORDER_NOT_CUSTOMIZING",
|
||||
"reasonName": "订单不在定制中"
|
||||
}
|
||||
],
|
||||
"groupVehicleRequirementId": "2099927498304131073",
|
||||
"groupVehicleRequirementStatus": "DRAFT",
|
||||
"groupVehicleRequirementVersion": 4
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
整团免车或没有豁免户时,`vehicleExemptHouseholds` 为空数组(不是 `null`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2099927193172815873",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"ready": true,
|
||||
"missing": [],
|
||||
"vehicleWaived": false,
|
||||
"vehicleMissing": [],
|
||||
"vehicleExemptHouseholds": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
无需求确认权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 本接口只读,不写库,可安全重复调用。
|
||||
- 豁免户**不进** `vehicleMissing`、**不影响** `ready`;它是提示信息,不是缺失。
|
||||
- `vehicleExemptHouseholds` 的户集合是「在团需车户」(含已完成订单),`REQUIREMENT_FROZEN` 只在团期已过资源准备时出现(例如物料准备中)。
|
||||
- 没有活跃正式用车需求(`vehicleMissing` 里有 `GROUP_REQUIREMENT_NOT_FOUND`)时,豁免户清单照常列出。
|
||||
- 打回后未重提的户算未提交:冻结期内它仍可重提,所以照报 809122,不进豁免户清单。
|
||||
- `POST .../requirement/confirm` 与本接口同一判定:存在 809122 条目时确认被拒、零写入;只有豁免户未提交时确认照常通过。
|
||||
|
||||
---
|
||||
|
||||
### 3. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `Path 参数 → GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑弹窗里点「自动汇总」时调用,按各子订单的 active 行程用车需求汇总出一份可原样 PUT 的草稿,零写入。本次起豁免户不进汇总、不报 809121,改列在 `exemptHouseholds`;809121 只报「本可以提交却没提交」及车型 / 日期 / 人数缺失的户。鉴权与 `GET .../vehicle-requirement` 同码。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.groupBatchId | String | 团期 ID |
|
||||
| data.currentStatus | String | 当前正式需求状态;未形成时为 `null` |
|
||||
| data.draft | Object | 汇总草稿,与保存请求体同形(`version` / `remark` / `groups`);豁免户不在任何分组里 |
|
||||
| data.droppedFleetItems[] | Array | 被丢弃的车型项(与改前一致) |
|
||||
| data.staleHeadcountOrders[] | Array | 实时人数与冻结人数不同的户(与改前一致) |
|
||||
| data.paddedOrderDays[] | Array | 补进分组的日期(与改前一致) |
|
||||
| data.violations[] | Array | 草稿预检违规(与保存同一份校验) |
|
||||
| data.exemptHouseholds[] | Array | **本次新增**:豁免户,按 orderId 升序;无则空数组 `[]` |
|
||||
| data.exemptHouseholds[].orderId | String | 子订单 ID |
|
||||
| data.exemptHouseholds[].orderNo | String | 子订单号 |
|
||||
| data.exemptHouseholds[].reason | String | `ORDER_NOT_CUSTOMIZING` 或 `REQUIREMENT_FROZEN`,见六.5 |
|
||||
| data.exemptHouseholds[].reasonName | String | 豁免原因中文名 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2099751749718822913/vehicle-requirement/aggregate-draft HTTP/1.1
|
||||
Host: <网关地址>
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试环境实测(该团唯一的需车户订单待出发、没提交行程用车需求,被列为豁免户,因此 809121 不再触发;草稿零分组,保存同一份校验报 809103):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2099751749718822913",
|
||||
"currentStatus": null,
|
||||
"draft": { "version": null, "remark": null, "groups": [] },
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": [
|
||||
{
|
||||
"code": 809103,
|
||||
"reason": "NO_GROUP",
|
||||
"detail": "本团存在需要用车的子订单,至少要提交一个乘车分组",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": null
|
||||
}
|
||||
],
|
||||
"exemptHouseholds": [
|
||||
{
|
||||
"orderId": "2099751749693657090",
|
||||
"orderNo": "HL20260915144643265",
|
||||
"reason": "ORDER_NOT_CUSTOMIZING",
|
||||
"reasonName": "订单不在定制中"
|
||||
}
|
||||
]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
全部需车户都已提交时 `exemptHouseholds` 为空数组;团里没有在团户时草稿 `groups` 为空数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2099751749718822913",
|
||||
"currentStatus": null,
|
||||
"draft": { "version": null, "remark": null, "groups": [] },
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": [],
|
||||
"exemptHouseholds": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
有户本可以提交却没提交(测试环境实测原文;同团另有 2 户已完成订单被豁免,不在清单里):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 「第30期 T29 #7441 AC-29 batch6」 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:26-0674:未提交行程用车需求、26-7797:未提交行程用车需求",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
车型字典不可用:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809120,
|
||||
"message": "车队车型字典暂不可用,无法校验车型,请稍后重试",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 本接口只读,不写库。
|
||||
- 809121 的户数只计未提交户与车型 / 日期 / 人数缺失的户,不计豁免户;报文按团号列户。
|
||||
- 豁免户不进草稿分组,草稿原样 PUT 时也不会因为豁免户没被覆盖而报 809109。
|
||||
- 豁免户与 809123(保存)/ 809122(预检)是同一份判定:汇总放行的户,保存时也不会被判成未提交。
|
||||
- `REQUIREMENT_FROZEN` 可能出现在本接口(团期已过资源准备时);同一份草稿在该阶段不能保存(保存只允许资源准备中)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写**后端返回什么、前端据什么判定**。
|
||||
|
||||
### 「保存正式用车需求」按钮的置灰条件
|
||||
|
||||
以预检接口为判据,满足任一条即置灰保存按钮并提示原因:
|
||||
|
||||
| 条件 | 判定写法 | 提示 |
|
||||
|------|----------|------|
|
||||
| 有未提交户 | `confirm-check` 的 `vehicleMissing.some(i => i.reason === 'HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED')` | 列出这些条目的 `orderNo`,提示先让定制师提交行程用车需求(保存会报 809123) |
|
||||
| 阶段不可保存 | `batchStatus !== 'RESOURCE_PREPARING'` | 仅资源准备中可编辑正式用车需求 |
|
||||
|
||||
豁免户**不是**置灰条件:`vehicleExemptHouseholds` 非空时按钮照常可点。
|
||||
|
||||
### ✅ 正确 / ❌ 错误的判定写法
|
||||
|
||||
| 场景 | 判定写法 |
|
||||
|------|----------|
|
||||
| ✅ 判「这户需要催定制师」 | `vehicleMissing[].reason === 'HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED'`,户取 `orderId` / `orderNo` |
|
||||
| ✅ 判「这户没交但不用催」 | 在 `vehicleExemptHouseholds[]` / `exemptHouseholds[]` 里;原因展示 `reasonName` |
|
||||
| ✅ 豁免户展示 | 在弹窗 / 预检横幅里单独列一块「以下户未参与排车」,逐户显示订单号 + `reasonName`;不要混进缺失清单 |
|
||||
| ❌ 用 `exemptHouseholds` 判「能不能保存」 | 豁免户不阻断保存,能不能保存看 809123 与 `vehicleMissing` |
|
||||
| ❌ 从 809123 / 809121 报文里抠订单号 | 报文只带团号,结构化户信息读 `vehicleMissing[].orderNo` / `exemptHouseholds[].orderNo` |
|
||||
| ❌ 假设豁免户清单在读接口 `GET .../vehicle-requirement` 里有值 | 该字段只在保存响应里填充,读接口为 `null` |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 无 DDL、无 Flyway 脚本、无新表 / 新列 / 新状态值。
|
||||
- `PUT .../vehicle-requirement`:809123 在任何写入之前抛出,整笔事务零写入;保存成功时的写库行为(失活旧版本 + 插新版本与分组 / 逐日行)与改前一致,豁免户清单不落库,是保存那一刻按户侧状态算出来的。
|
||||
- `GET .../confirm-check`、`GET .../aggregate-draft`:只读,零写入。
|
||||
- 判定读的是既有数据:订单状态(`order_main.order_status`)、团期状态(`order_group_batch.batch_status`)、行程用车需求行(`order_vehicle_requirement`,含历史版本判断是否被打回)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 团期不存在 → HTTP 200 + `code=589500`。
|
||||
- 无权限 → HTTP 200 + `code=589507`。
|
||||
- 豁免户清单在「无豁免户」时是空数组 `[]`;`GET .../vehicle-requirement` 等非保存响应里 `exemptHouseholds` 为 `null`。
|
||||
- 整团免车(`vehicleWaived=true`)时车侧整体跳过,`vehicleExemptHouseholds` 为空数组。
|
||||
- 全团需车户都是豁免户:保存空分组报 809103,分组成员又不能为空;按现行口径把豁免户编进分组或走整团免车。
|
||||
- 809123 报文的户标识:团号 → 订单号 → 「某子订单」,不含雪花 ID。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### exemptHouseholds[].reason / vehicleExemptHouseholds[].reason(豁免原因)
|
||||
|
||||
**所属字段**: `GroupVehicleExemptHouseholdVO.reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(reasonName) | 说明 | 出现在哪些接口 |
|
||||
|----|------|------|------|
|
||||
| `ORDER_NOT_CUSTOMIZING` | 订单不在定制中 | 订单状态不是定制中(待支付 / 待出发 / 出行中 / 已完成),定制师提交行程用车需求会被 582017 拒 | 保存、预检、自动汇总 |
|
||||
| `REQUIREMENT_FROZEN` | 团期需求已冻结且该户未被打回 | 团期已过资源准备(物料准备中及之后),该户从没提交过或最新版本未被打回,定制师提交会被 589536 拒 | 仅预检、自动汇总;保存接口不会出现 |
|
||||
|
||||
### 新增错误码
|
||||
|
||||
| 错误码 | 模板 | 说明 |
|
||||
|------|------|------|
|
||||
| 809123 | `{0}有 {1} 户尚未提交行程用车需求,暂不能保存正式用车需求:{2}` | `{0}` 团期人话名(如 `团期「第30期 …」`,快照缺失为 `该团期`);`{1}` 户数;`{2}` 逐户团号,顿号分隔,无团号回落订单号 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `GroupVehicleRequirementRespVO.exemptHouseholds` | 不存在 | 保存响应填充(数组),其它响应为 `null` |
|
||||
| `GroupBatchRequirementCheckRespVO.vehicleExemptHouseholds` | 不存在 | 恒为数组 |
|
||||
| `GroupVehicleAggregateDraftRespVO.exemptHouseholds` | 不存在 | 恒为数组 |
|
||||
| 错误码 809123 | 不存在 | 保存时有未提交户 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 保存时有户能交没交 | 不检查;若该户没被分组覆盖报 809109,逼运营替它编成员 | 报 809123,点名这些户 |
|
||||
| 保存时有户交不了(如订单待支付) | 同上,必须编进分组 | 不阻断、不要求覆盖,列入 `exemptHouseholds` |
|
||||
| 809109 的检查对象 | 全部在团需车户 | 只检查已提交户 |
|
||||
| 预检 809122 | 凡没有 active 行程用车需求的放行户都报 | 只报未提交户;豁免户改列 `vehicleExemptHouseholds` |
|
||||
| 自动汇总 809121「未提交行程用车需求」 | 凡没有行程用车需求的需车户都报 | 只报未提交户;豁免户不进草稿、改列 `exemptHouseholds` |
|
||||
| 整团确认时只有豁免户没交 | 报 809122 拒绝 | 照常确认 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否(字段层面)。三个接口只新增字段;错误码只新增 809123。行为上保存会多一种拒绝(809123),前端按通用错误处理展示 `message` 即可正确工作。
|
||||
- **前端是否必须同步上线**: 否。不改前端时:809123 按通用错误弹窗展示;新增字段被忽略,豁免户不会显示在页面上(运营看不到哪些户没参与排车)。展示豁免户清单与按钮置灰需要前端改动。
|
||||
- **前端 workaround 清理点**: 若页面有「把没提交的户也塞进分组才能保存」的操作引导,可以撤掉。
|
||||
- **风险面**: 豁免户不参与排车,真要用车但订单尚在待支付的户需要运营从豁免户清单里看到并跟进。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 上述三个管理后台接口,以及与预检同一判定的 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm`(请求 / 响应结构不变,只是豁免户不再触发 809122)。
|
||||
- **零影响**:
|
||||
- 小程序端(mp)全部接口。
|
||||
- 子订单级行程用车需求提交 / 打回链路:582017、589536 的触发条件不变。
|
||||
- `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 读接口、撤回、免车、重开:结构不变(`exemptHouseholds` 为 `null`)。
|
||||
- 接送机(TRANSFER)相关:只看行程用车(TRAVEL)。
|
||||
- 数据库:无 DDL、无迁移。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:`hl-order-service-v3` @ `dev-v3` `c0be69b02`,2026-09-24 11:04 两个实例(8086 / 8186)均 UP。经网关(自签 admin token)实测,全部为只读或被拒的写:
|
||||
|
||||
```
|
||||
PUT /v3/admin/order/group-batch/2099927193172815873/vehicle-requirement(version=4,只放已提交户)
|
||||
→ 200;code=809123「团期「第30期 T29 #7441 AC-29 batch6」有 2 户尚未提交行程用车需求,
|
||||
暂不能保存正式用车需求:26-0674、26-7797」;两个实例返回一致 ✓
|
||||
→ 同团 2 户已完成订单未被点名 ✓;库里正式需求仍为 v4 DRAFT active(零写入)✓
|
||||
→ 不带 token → 401「缺少有效的 Authorization 头」✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2099927193172815873/requirement/confirm-check
|
||||
→ 200;vehicleMissing 的 809122 只有 26-0674 / 26-7797 两户 ✓
|
||||
→ vehicleExemptHouseholds = HL20260916022359336、HL20260916022401707(ORDER_NOT_CUSTOMIZING)✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2099958693339566081/requirement/confirm-check(物料准备中)
|
||||
→ 200;定制中且从没提交的 HL20260916042902319 不报 809122,
|
||||
列入 vehicleExemptHouseholds(REQUIREMENT_FROZEN)✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2099927193172815873/vehicle-requirement/aggregate-draft
|
||||
→ 200;code=809121,只数到 2 户(26-0674、26-7797),豁免户不在清单 ✓
|
||||
|
||||
GET /v3/admin/order/group-batch/2099751749718822913/vehicle-requirement/aggregate-draft
|
||||
→ 200;exemptHouseholds 列出 HL20260915144643265(ORDER_NOT_CUSTOMIZING),不报 809121 ✓
|
||||
```
|
||||
|
||||
保存成功路径(2026-09-24 15:0x,自建团期,团期名前缀 `8219-AC5-jw-`,均为资源准备中):
|
||||
|
||||
```
|
||||
团期 2103018513226752001(A、B 定制中,C 待支付且无行程用车需求)
|
||||
PUT(分组只放 A)
|
||||
→ 200;code=809123「…有 1 户尚未提交行程用车需求…:HL20260924150740940」,只点名 B、不含 C;零写入 ✓
|
||||
B 提交行程用车需求后 PUT(分组放 A、B)
|
||||
→ 200;code=200,DRAFT v1;exemptHouseholds 只有 C(HL20260924150741326,ORDER_NOT_CUSTOMIZING)✓
|
||||
→ GET 回读:分组成员只有 A、B,C 不在任何分组 ✓
|
||||
|
||||
团期 2103018935689650178(两户都已提交)
|
||||
PUT(分组放两户)
|
||||
→ 200;code=200,DRAFT v1;exemptHouseholds = [] ✓;回读两户都在分组成员里 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8219](https://git.1814.love/wx/HL/issues/8219)
|
||||
- 关联 PR: [wx/HL#8317](https://git.1814.love/wx/HL/pulls/8317)、[wx/HL#8319](https://git.1814.love/wx/HL/pulls/8319)
|
||||
- 809122 的来源:`changelogs-v2/2026-09/23_8249_查看需求页未提交状态与户级用车预检-修改接口-管理后台.md`
|
||||
- 自动汇总端点:`changelogs-v2/2026-09/23_8220_团期正式行程用车需求自动汇总草稿-新增接口-管理后台.md`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8219](https://git.1814.love/wx/HL/issues/8219)
|
||||
- **PR**: [#8317](https://git.1814.love/wx/HL/pulls/8317)、[#8319](https://git.1814.love/wx/HL/pulls/8319)
|
||||
- **Merge commit**: [c0be69b02](https://git.1814.love/wx/HL/commit/c0be69b02809baabb0a557f3d940d092e4344604)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,571 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8235"
|
||||
title: "看板订单记录新增团车整段接管标记;已接管且零派车行的行程用车在看板与矩阵不再生成虚拟待派卡"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "3f3f14b3cddf5a07e09730173f896084af3f5832"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "前端已交付:normalizeBoardOrder 显式归一布尔;isGroupVehicleCovered 只认 === true;卡片状态区「团车接管·可释放」badge+表格配车列 chip/popover 说明行;矩阵抑制与计数减少纯后端,前端无本地对比旧计数逻辑;spec 18 例全绿+fleet/board 557 例全绿,checkpoint 7 项过"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 团车整段接管标记透出 + 虚拟待派卡抑制
|
||||
|
||||
> **存放目录**: `changelogs-v2/{YYYY-MM}/`
|
||||
>
|
||||
> **服务**: hl-fleet-service (端口 8087)
|
||||
> **PR**: #8300
|
||||
> **Issue**: #8235
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「车务看板」订单列表/网格视图、「车辆矩阵」未派清单与网格/月度统计
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**团车已整段接管、且在 fleet 侧零派车行的行程用车(TRAVEL)需求,不再在看板与矩阵里生成虚拟待派卡**,也不计入矩阵未派计数。这是主动抑制,不是数据丢失:该需求的车已经由团车统一安排,逐单侧没有活儿可派。若该户在团确认前已经被逐单单独派了车(真实派车行仍存在),这行**照常出卡**,并在看板订单记录上新增 `groupVehicleCovered=true` 标记,提示车务这行属冗余、可释放;只在这种"有真实派车行"的场景下才会看到该标记为 `true`,其余情况(未接管、接送机卡、虚拟待派卡)一律为 `null`,不会出现 `false`。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 看板订单列表 | GET | `/admin/fleet/board/orders` | 响应新增字段+行为变更 | 新增 `groupVehicleCovered`;团车已接管的零派车行 TRAVEL 不再生成虚拟待派卡 |
|
||||
| 2 | 矩阵未派订单清单 | GET | `/admin/fleet/matrix/unassigned-orders` | 行为变更 | 同上抑制逻辑,命中的订单不再出现在未派清单 |
|
||||
| 3 | 矩阵网格主数据 | GET | `/admin/fleet/matrix/grid` | 行为变更 | `statusCounts` 未派/总量计数不再计入被抑制的虚拟条目 |
|
||||
|
||||
> `GET /admin/fleet/matrix/month-counts` 与本清单第 3 项共用完全相同的 `statusCounts` 计算口径(`MatrixController` 自身 javadoc 明确"每月 statusCounts 与相同筛选下 grid.statusCounts 同口径"),受同一行为变更影响,不单列小节,详情参照第 3 项。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 看板订单列表 `GET /admin/fleet/board/orders`
|
||||
|
||||
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`(`records: BoardOrderRecordVO[]`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车务看板」订单列表/网格视图(`variant=list|grid`),车务人员按状态、日期、团号、车型等筛选待派/已派订单卡片,逐卡执行改派、派车、查看行程等操作。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| statuses | Query | String[] | ❌ | 枚举:unassigned/unassigned_urgent/holding/holding_urgent/assigned/canceled/completed,可多选 | 状态多选筛选 |
|
||||
| status | Query | String | ❌ | 同上枚举,兼容单值 | statuses 兼容别名 |
|
||||
| startDayFrom | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期区间起 |
|
||||
| startDayTo | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期区间止 |
|
||||
| startDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | startDayFrom 兼容别名 |
|
||||
| endDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | startDayTo 兼容别名 |
|
||||
| vehicleTypeKeys | Query | String[] | ❌ | 车型大类字典值 | 车型多选筛选 |
|
||||
| typeKeys | Query | String[] | ❌ | 同上 | vehicleTypeKeys 兼容别名 |
|
||||
| driverName | Query | String | ❌ | - | 司机姓名模糊搜索 |
|
||||
| keyword | Query | String | ❌ | - | 统一关键字搜索 |
|
||||
| contactName | Query | String | ❌ | - | 联系人姓名模糊搜索 |
|
||||
| contactKeyword | Query | String | ❌ | - | contactName 兼容别名 |
|
||||
| teamNo | Query | String | ❌ | - | 团号模糊搜索 |
|
||||
| groupBatchId | Query | Long | ❌ | 等值匹配,只认派车行建行时刻快照(#7443) | 运营团期 ID 筛选 |
|
||||
| consultantId | Query | Long | ❌ | - | 定制师 ID 精确筛选 |
|
||||
| plannerName | Query | String | ❌ | - | 定制师姓名模糊搜索 |
|
||||
| consultantName | Query | String | ❌ | - | plannerName 兼容别名 |
|
||||
| variant | Query | String | ❌ | `list`/`grid`,其余值走缺省 100001 视图 | 视图口径 |
|
||||
| page(或 pageNo) | Query | Integer | ❌ | ≥1,缺省 1 | 页码 |
|
||||
| pageSize | Query | Integer | ❌ | 1-100 | 每页条数 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 派车行/虚拟条目主键(虚拟条目为合成 ID) |
|
||||
| orderId | Long | 子订单 ID |
|
||||
| orderNo | String | 订单编号 |
|
||||
| teamNo | String | 团号 |
|
||||
| groupBatchId | Long | 运营团期 ID(#7443;非团期订单为 null) |
|
||||
| customerName | String | 客户姓名 |
|
||||
| assignmentId | Long | 派车行 ID;虚拟条目恒为 null |
|
||||
| assignmentGroupId | Long | 派车分组 ID |
|
||||
| requirementId | Long | 当前用车需求 ID |
|
||||
| assignmentStatus | String | 派单状态码 |
|
||||
| virtualPending | Boolean | 真实记录恒显式 `false`(非 null);虚拟待派卡为 `true` |
|
||||
| groupVehicleCovered | Boolean | 新增(#8235)。仅真实记录且该需求已被团车整段接管时为 `true`;未接管、接送机卡、虚拟待派卡一律为 `null`,永不出现 `false` |
|
||||
| urgentBadge | String | 紧急标签 |
|
||||
| canAssign | Boolean | 是否可派车 |
|
||||
| dispatchReadOnly | Boolean | 是否只读(不可操作) |
|
||||
| startDate / endDate | LocalDate | 行程起止日期 |
|
||||
| currentVehiclePlate / currentDriverName | String | 当前车辆车牌/司机姓名 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /admin/fleet/board/orders?statuses=unassigned&variant=list&page=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": 9900000000000101,
|
||||
"orderId": 9900000000000201,
|
||||
"orderNo": "HL20261001000001",
|
||||
"teamNo": "26-9901",
|
||||
"groupBatchId": 9900000000000301,
|
||||
"customerName": "示例客户A",
|
||||
"assignmentId": 9900000000000401,
|
||||
"assignmentGroupId": 9900000000000501,
|
||||
"requirementId": 9900000000000601,
|
||||
"assignmentStatus": "PROCESSING",
|
||||
"virtualPending": false,
|
||||
"groupVehicleCovered": true,
|
||||
"urgentBadge": null,
|
||||
"canAssign": true,
|
||||
"dispatchReadOnly": false,
|
||||
"startDate": "2026-11-01",
|
||||
"endDate": "2026-11-05",
|
||||
"currentVehiclePlate": "蒙K·A0001",
|
||||
"currentDriverName": "示例司机"
|
||||
},
|
||||
{
|
||||
"id": -9900000000000701,
|
||||
"orderId": 9900000000000801,
|
||||
"orderNo": "HL20261001000002",
|
||||
"teamNo": null,
|
||||
"groupBatchId": null,
|
||||
"customerName": "示例客户B",
|
||||
"assignmentId": null,
|
||||
"assignmentGroupId": null,
|
||||
"requirementId": 9900000000000901,
|
||||
"assignmentStatus": "unassigned",
|
||||
"virtualPending": true,
|
||||
"groupVehicleCovered": null,
|
||||
"urgentBadge": null,
|
||||
"canAssign": true,
|
||||
"dispatchReadOnly": false,
|
||||
"startDate": "2026-11-02",
|
||||
"endDate": "2026-11-06",
|
||||
"currentVehiclePlate": null,
|
||||
"currentDriverName": null
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
无匹配记录时返回空 `records` 数组,`total=0`,不报错。虚拟待派卡取数失败(Nacos `fleet.board.virtual-candidates-enabled=false` 或 order-v3 虚拟候选不可用)时只丢虚拟条目,真实派车行照常返回——这一条与矩阵未派清单口径一致。
|
||||
|
||||
**带日期筛选时另有一条既有 fail-closed 口径(非本次引入,本次未改)**:看板按日期筛选走单独的日期候选扫描,该扫描取 order-v3 日期候选页失败,或候选行数据不完整(派车行 `requirement_id` 为空、订单 ID 为空、候选键为空或重复)时,本页整体返回空 `records`、`total=0`、`code=200`,不报错。因此带日期筛选时的空列表**不能等同于**「该日期窗确实无单」;不带日期筛选的查询与矩阵接口不走这条分支,不受影响。测试服当前就有一条命中该口径的夹具数据,见「八、测试环境已验证」末段与 #8301。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `groupVehicleCovered` 只在**真实记录**(`virtualPending=false`)且该需求已被团车整段接管时为 `true`;虚拟待派卡(`virtualPending=true`)恒为 `null`。判断"是否已接管"请只用 `=== true`,不要用 `!== true` 当作"未接管"的充分条件去做业务分支——`null` 同时覆盖"未接管"与"该卡不适用本标记"两种情况。
|
||||
- 团车已接管但零派车行的 TRAVEL 需求:本接口不再返回该需求的虚拟待派卡(该记录会从列表里消失)。
|
||||
- 团车已接管但存在存量真实派车行的 TRAVEL 需求(团确认前已逐单派出的部分):该行照常出卡,`groupVehicleCovered=true`,车务应据此判断该行属冗余可释放;车辆矩阵占用与行归属不受影响。
|
||||
- 非团期订单、以及团期内的接送机(TRANSFER)需求不受本次改动影响,出卡逻辑与计数均不变。
|
||||
- 鉴权:未登录 401(网关拦截)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 矩阵未派订单清单 `GET /admin/fleet/matrix/unassigned-orders`
|
||||
|
||||
**VO**: `MatrixUnassignedReqVO → List<MatrixUnassignedOrderVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车辆矩阵」未派窗口,车务人员按年月查看当月全部未派/待派订单,从中选取虚拟待派卡发起整单派车。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| year | Query | Integer | ✅ | 1970-9999 | 年份 |
|
||||
| month | Query | Integer | ✅ | 1-12 | 月份 |
|
||||
| typeKeys | Query | String[] | ❌ | 车型大类字典值 | 车型多选筛选 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | String | 订单 ID(订单号) |
|
||||
| orderNumericId | Long | 订单数字 ID |
|
||||
| orderNo | String | 订单编号 |
|
||||
| teamNo | String | 团号 |
|
||||
| virtualPending | Boolean | `true`=零派车行的虚拟待派卡(`assignmentId` 为 null,走 requirementId 整单派车入口) |
|
||||
| assignmentId | Long | 派车行 ID;虚拟条目为 null |
|
||||
| requirementId | Long | 用车需求 ID |
|
||||
| vehicleCategory / categoryLabel | String | 车型大类编码/中文名 |
|
||||
| customerName | String | 客户姓名 |
|
||||
| productName | String | 产品名 |
|
||||
| headcountLabel | String | 人数摘要文案 |
|
||||
| startDate / endDate | LocalDate | 行程起止日期(未裁剪) |
|
||||
| assignmentStatus | String | 派单状态码 |
|
||||
| urgentBadge | String | 紧急标签 |
|
||||
| vehicleAdvice | Object | 车辆建议;M2 暂返 null |
|
||||
| parallelAssignments | Array | 并行派车条目;虚拟待派条目恒为空数组 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /admin/fleet/matrix/unassigned-orders?year=2026&month=11&typeKeys=suv
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"orderId": "9900000000000801",
|
||||
"orderNumericId": 9900000000000801,
|
||||
"orderNo": "HL20261001000002",
|
||||
"teamNo": null,
|
||||
"virtualPending": true,
|
||||
"assignmentId": null,
|
||||
"requirementId": 9900000000000901,
|
||||
"vehicleCategory": "suv",
|
||||
"categoryLabel": "SUV",
|
||||
"customerName": "示例客户B",
|
||||
"productName": "示例产品",
|
||||
"headcountLabel": "2大1小",
|
||||
"startDate": "2026-11-02",
|
||||
"endDate": "2026-11-06",
|
||||
"assignmentStatus": "unassigned",
|
||||
"urgentBadge": null,
|
||||
"vehicleAdvice": null,
|
||||
"parallelAssignments": []
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": [], "success": true }
|
||||
```
|
||||
|
||||
`vehicleAdvice` 暂返 null(数据源未建);Nacos `fleet.board.virtual-candidates-enabled=false` 或 order-v3 候选不可用时只丢虚拟条目,真实条目原样返回(fail-open,不让未派窗口整体打不开)——此为既有降级契约,本次未改。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "month: 月份必须在 1-12 之间",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 团车已整段接管且零派车行的 TRAVEL 需求不再出现在本清单(原本会以一条 `virtualPending=true` 的条目出现,现在整条消失)。
|
||||
- 该需求若存在存量真实派车行,本清单不受影响——本清单只承载"待派"的虚拟条目,真实已派条目不在本接口的返回范围内(走看板订单列表接口查看)。
|
||||
- 非团期订单、接送机(TRANSFER)需求不受影响。
|
||||
- 鉴权:需 `AdminContextUtil` 权限检查。
|
||||
|
||||
---
|
||||
|
||||
### 3. 矩阵网格主数据 `GET /admin/fleet/matrix/grid`
|
||||
|
||||
**VO**: `MatrixGridReqVO → MatrixGridRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车辆矩阵」主视图,按年月+车队/车型/状态筛选渲染车辆甘特图与顶部统计条。`statusCounts` 驱动顶部"未派/已派/合计"计数徽标。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| year | Query | Integer | ✅ | - | 年份 |
|
||||
| month | Query | Integer | ✅ | 1-12(越界由 Service 返 605010,非 400) | 月份 |
|
||||
| season | Query | String | ❌ | `active`/`pending`/`archived`/`blacklist`,缺省 active | 车辆季节状态 |
|
||||
| fleetTeamIds | Query | Long[] | ❌ | - | 车队 ID 多选 |
|
||||
| typeKeys | Query | String[] | ❌ | 车型大类字典值 | 车型多选 |
|
||||
| status | Query | String | ❌ | `all`/`unassigned`/`assigned` | 状态口径(粗粒度) |
|
||||
| statuses | Query | String[] | ❌ | `unassigned`/`unassigned_urgent`/`holding`/`holding_urgent`/`assigned`/`completed`;非空时优先于 status | 状态口径(细粒度) |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| year / month | Integer | 回显查询年月 |
|
||||
| daysInMonth | Integer | 当月天数(28-31) |
|
||||
| todayDay | Integer | 今日是第几天;非当月查询为 null |
|
||||
| weekendDays | Integer[] | 周末日序号列表 |
|
||||
| vehicles | Array | 车辆行数组(每辆车含其派车段/衔接/重叠子数组,本次未变更) |
|
||||
| fleetTeamCounts | Array | 车队维度计数,本次未变更 |
|
||||
| statusCounts.totalAssignments | Integer | 有效派车行总数 |
|
||||
| statusCounts.unassignedAssignments | Integer | 未派派车行数(含虚拟条目)——本次改动后计数减少:被团车整段接管且零派车行的虚拟条目不再计入 |
|
||||
| statusCounts.assignedAssignments | Integer | 已派派车行数 |
|
||||
| statusCounts.totalOrders | Integer | 去重订单总数 |
|
||||
| statusCounts.unassignedOrders | Integer | 含至少一个未派项的去重订单数——同上,计数口径同步减少 |
|
||||
| statusCounts.partialOrders | Integer | 部分派车的去重订单数 |
|
||||
| statusCounts.assignedOrders | Integer | 全部派车完成的去重订单数 |
|
||||
| statusCounts.effectiveStatusCounts | Object | 固定五键(unassigned/unassigned_urgent/holding/holding_urgent/assigned)分面计数 |
|
||||
| unassignedWindowCount | Integer | 未派窗口角标计数,口径与 unassignedOrders 一致 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /admin/fleet/matrix/grid?year=2026&month=11&season=active
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"year": 2026,
|
||||
"month": 11,
|
||||
"daysInMonth": 30,
|
||||
"todayDay": null,
|
||||
"weekendDays": [1, 7, 8, 14, 15, 21, 22, 28, 29],
|
||||
"vehicles": [],
|
||||
"fleetTeamCounts": [],
|
||||
"statusCounts": {
|
||||
"totalAssignments": 118,
|
||||
"unassignedAssignments": 6,
|
||||
"assignedAssignments": 112,
|
||||
"totalOrders": 95,
|
||||
"unassignedOrders": 5,
|
||||
"partialOrders": 2,
|
||||
"assignedOrders": 88,
|
||||
"effectiveStatusCounts": {
|
||||
"unassigned": 4,
|
||||
"unassigned_urgent": 1,
|
||||
"holding": 3,
|
||||
"holding_urgent": 0,
|
||||
"assigned": 112,
|
||||
"completed": 40
|
||||
}
|
||||
},
|
||||
"unassignedWindowCount": 5
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"year": 2026, "month": 11, "daysInMonth": 30, "todayDay": null,
|
||||
"weekendDays": [], "vehicles": [], "fleetTeamCounts": [],
|
||||
"statusCounts": {
|
||||
"totalAssignments": 0, "unassignedAssignments": 0, "assignedAssignments": 0,
|
||||
"totalOrders": 0, "unassignedOrders": 0, "partialOrders": 0, "assignedOrders": 0,
|
||||
"effectiveStatusCounts": {"unassigned":0,"unassigned_urgent":0,"holding":0,"holding_urgent":0,"assigned":0,"completed":0}
|
||||
},
|
||||
"unassignedWindowCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
虚拟候选源不可用时按同一 `BoardCandidateSource` 降级契约 fail-open:只丢虚拟条目对 `statusCounts` 未派计数的贡献,真实派车行的统计不受影响。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605010,
|
||||
"message": "月份必须在 1-12 之间",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `statusCounts` 的未派相关计数(`unassignedAssignments`/`unassignedOrders`/`unassignedWindowCount`,以及 `effectiveStatusCounts.unassigned`/`unassigned_urgent`)在本次改动后会同步减少:被团车整段接管且零派车行的虚拟条目不再参与统计。这不是数据异常,前端若有本地缓存/对比旧计数的逻辑需知悉此变化。
|
||||
- `assignedAssignments`/`assignedOrders` 等已派计数不受影响;`vehicles[]` 车辆行的段位数据与占用计算不受影响。
|
||||
- `GET /admin/fleet/matrix/month-counts` 与本接口共用完全相同的 `statusCounts` 计算口径,受同一行为变更影响。
|
||||
- 鉴权:需 `AdminContextUtil` 权限检查。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误用法对照
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| ✅ 判断是否显示"可释放"提示 | `record.groupVehicleCovered === true` |
|
||||
| ✅ 判断"不显示提示" | `record.groupVehicleCovered !== true`(覆盖 `null` 与字段不存在两种情况) |
|
||||
| ❌ 用 `groupVehicleCovered === false` 做判断分支 | 该值永不出现 `false`,只会是 `true` 或 `null`,这样写的分支永远走不到 |
|
||||
| ❌ 把矩阵未派计数的减少当作接口故障 | 团车已接管、零派车行的订单不再计入未派计数,是预期行为,不是数据丢失 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无。三个接口均为只读 GET,`groupVehicleCovered` 与虚拟待派卡的抑制均由后端在读取时实时计算,不接受前端传参覆盖,也无需前端在请求前做任何字段置空等准备动作。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
三个接口均为只读查询,无新增或变更的写库行为。`groupVehicleCovered` 由 `hl-fleet-service` 在读取时实时计算:按看板卡片当前用车需求 ID 匹配该订单从 `hl-order-service-v3` 经 Feign 聚合拿到的需求身份集合,取其"是否已被团车整段接管"标记;不产生新表,也不改变既有列的语义。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 无匹配记录 → 200 + 空数组/空列表,不报错
|
||||
- 矩阵网格 `month` 越界(不在 1-12)→ 605010 业务错误码,非 400
|
||||
- 虚拟候选源(order-v3 侧)不可用或 Nacos 开关关闭 → fail-open,只丢虚拟条目,真实记录与真实计数原样返回
|
||||
- 看板带日期筛选时,日期候选扫描取数失败或候选行不完整 → 200 + 整页空(既有 fail-closed,非本次引入,见接口 1「空数据 / 降级响应」;矩阵接口不走该分支)
|
||||
- 老数据兼容:历史已生成的虚拟待派卡在下次读取时按新逻辑重新计算,不残留旧结果;`groupVehicleCovered` 对改动前的历史真实记录同样实时计算,无需迁移
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### groupVehicleCovered(团车整段接管标记)
|
||||
|
||||
**所属字段**: `BoardOrderRecordVO.groupVehicleCovered` | **类型**: `Boolean`
|
||||
|
||||
本次不新增字典枚举类;该字段是只会取两种取值的标记位(不是完整的三态布尔):
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `true` | 仅真实记录:该需求已被团车整段接管,此行属冗余可释放 |
|
||||
| `null` | 未接管 / 接送机卡 / 虚拟待派卡 / 不适用;`false` 永不出现 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 | 变化说明 |
|
||||
|------|------|------|----------|
|
||||
| `BoardOrderRecordVO.groupVehicleCovered` | 字段不存在 | 新增,`true`/`null` 两态 | 仅真实记录且被团车整段接管时为 `true` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 | 影响 |
|
||||
|------|------|------|------|
|
||||
| 团车已接管、零派车行的 TRAVEL 需求 | 在看板订单列表 / 矩阵未派清单里生成一张虚拟待派卡 | 不再生成虚拟待派卡,该条目从列表中消失 | 影响 3 个接口的记录集合与矩阵计数 |
|
||||
| 团车已接管、存在存量真实派车行的 TRAVEL 需求 | 照常出卡,无标记区分冗余 | 照常出卡,新增 `groupVehicleCovered=true` 标记 | 仅看板订单列表新增标记,出卡数量不变 |
|
||||
| 矩阵 `statusCounts` 未派相关计数 | 含上述被抑制的虚拟条目 | 不含 | `unassignedAssignments`/`unassignedOrders`/`unassignedWindowCount`/`effectiveStatusCounts.unassigned*` 同步减少 |
|
||||
| 非团期订单 / 接送机需求 | 不受影响 | 不受影响 | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。`groupVehicleCovered` 是新增字段,旧前端未读取该字段不受影响;虚拟卡抑制导致的记录/计数减少是数据层面的行为变化,不改变接口结构或错误码。
|
||||
- **前端是否必须同步上线**: 是(针对希望展示"可释放"提示的场景)。若前端有基于虚拟待派卡数量做的本地校验或缓存对比逻辑,需知悉计数会因本次改动而减少。
|
||||
- **前端 workaround 清理点**: 无——这是新增能力,此前前端没有对应的字段或逻辑需要清理。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:
|
||||
- 管理后台「车务看板」订单列表/网格视图(`/admin/fleet/board/orders`)
|
||||
- 管理后台「车辆矩阵」未派窗口(`/admin/fleet/matrix/unassigned-orders`)
|
||||
- 管理后台「车辆矩阵」网格与月度统计条(`/admin/fleet/matrix/grid`、`/admin/fleet/matrix/month-counts`)
|
||||
|
||||
- **零影响**:
|
||||
- 团期本身的团级配车流程与团级基线计算
|
||||
- 车辆矩阵的车辆占用/甘特图段位计算(`vehicles[]` 结构与数据不变)
|
||||
- 接送机(TRANSFER)需求的看板/矩阵可见性与计数
|
||||
- 非团期订单的看板/矩阵可见性与计数
|
||||
- 看板订单详情(`/admin/fleet/board/orders/{orderId}`)、出行人列表、操作时间线等其余看板接口
|
||||
- C 端与定制师端相关接口
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
测试服部署:`hl-order-service-v3` 2026-09-24 02:52:37 / `hl-fleet-service` 2026-09-24 02:53:43,均部署到 commit `ea9cd0d33`(deploy-status 均为 ok;两服务必须同批部署,fleet 侧的接管判定依赖 order-v3 的打标)。
|
||||
|
||||
```
|
||||
GET /admin/fleet/board/orders(不带日期筛选,全量翻页,共 243 条记录)
|
||||
→ groupVehicleCovered 键在全部 243 条记录中均存在(0 条缺失)
|
||||
→ 取值只有 true(5 条,均为真实派车行)与 null(205 条真实卡 + 33 条虚拟待派卡)
|
||||
→ 0 条记录取值为 false ✓
|
||||
```
|
||||
|
||||
```
|
||||
GET /admin/fleet/matrix/unassigned-orders?year=Y&month=M(2026-09-24 03:50~03:51,2026-08 至 2027-08 逐月 13 次调用)
|
||||
→ 13 次均 HTTP 200 / code=200
|
||||
→ 12 张被团车整段接管、fleet 侧零派车行的订单(出行日期落在 2026-10、2026-11、2027-03,均在查询月份内)
|
||||
在 13 个月的响应原文中出现 0 次 ✓
|
||||
→ 对照组 4 张未被接管的零派车行订单(出行 2026-11-15~11-17)全部出现在 2026-11 的未派清单中 ✓
|
||||
GET /admin/fleet/matrix/grid?year=2026&month=11
|
||||
→ HTTP 200 / code=200,statusCounts 正常下发(unassignedOrders=23,与同月未派清单 23 条一致)✓
|
||||
```
|
||||
矩阵接口不走看板的日期候选扫描分支,下方 #8301 的限定对矩阵不适用。
|
||||
|
||||
已知测试环境限定(与本次改动无关,独立跟踪于 #8301):看板按日期筛选查询,筛选窗口与 2026-11-05~2026-11-07 有交集时,因测试夹具数据缺陷(某条派车行的 `requirement_id` 为 null)导致该日期筛选分支整体返回 0 条;不带日期筛选的查询不受影响,本次改动的验证结论(上方 243 条全量核对)不依赖该日期区间。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8235](https://git.1814.love/wx/HL/issues/8235)
|
||||
- 关联 Issue(测试环境已知缺口,独立跟踪): [wx/HL#8301](https://git.1814.love/wx/HL/issues/8301)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8235](https://git.1814.love/wx/HL/issues/8235)
|
||||
- **PR**: [#8300](https://git.1814.love/wx/HL/pulls/8300)
|
||||
- **Merge commit**: [ea9cd0d33](https://git.1814.love/wx/HL/commit/ea9cd0d33)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,453 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8248"
|
||||
title: "尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸按主报账人豁免 + 财务应收台账"
|
||||
consumer: "admin"
|
||||
author: "yst"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "b556da24791ca0ecf2393ef0e8505989c7996f8a"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "尾款收款模型重构 Epic(方案甲·全实时)。PR-6a #8274(581062) + PR-5a #8263(584082) + PR-3 #8207 合并 + PR-5b #8325/②#8336/③#8328 部署。部署:hl-order-service-v3 dev-v3 @ baf643412,2026-09-24 滚动部署(8086/8186 均 UP)。部署后 fin↔order 对账 D1~D7 现网零漂移。 | 2026-09-24 mmg 交付:①LockModal 无主报账人选人必填化(星号+引导+置灰双保险);②两接口 JSDoc 补 581062/584082 新口径;③新增应收台账只读页(api/finance/receipt.js+finance/receipt/receivable,原型 recv-ledger 对齐,代收欠收互斥直显,订单号链接钻取);spec 5+3+2 例全绿;⚠菜单「收款管理/应收台账」依赖后端 sys_menu 补配(changelog 未提及,请后端补配)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸豁免 + 财务应收台账
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(订单核心域 + 核单域)· hl-finance(收款域)
|
||||
> **Issue**: https://git.1814.love/wx/HL/issues/8248 (主) · https://git.1814.love/wx/HL/issues/8265 · https://git.1814.love/wx/HL/issues/8191
|
||||
> **PR**: https://git.1814.love/wx/HL/pulls/8274 · https://git.1814.love/wx/HL/pulls/8263 · https://git.1814.love/wx/HL/pulls/8207
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「订单确认行程弹框」「核单提交」「财务应收台账」三处
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(前端必读)
|
||||
|
||||
- **确认行程前必须先有主报账人**:普通订单点「确认行程」时,如果本单还没有主报账人且弹框里也没选人,接口直接报 **581062「请先指定本单主报账人(尾款代收人)」**,确认动作不发生。确认弹框必须提供主报账人选择项并传 `reporterAssignmentId`。
|
||||
- **核单欠收硬闸放行面扩大**:提交核单时「尾款未收齐」不再一律拦死——只要本单已指定主报账人,欠收部分记为主报账人代收口径,允许提交核单;**只有「欠钱且无人兜底(无主报账人)」的单**才会继续被 584082 拦截。
|
||||
- **新增财务应收台账只读分页接口**:`GET /admin/finance/receipt/receivable/page`,订单维度看应收/已收/代收/欠收,代收列与欠收列互斥(指定主报账人后欠收挪入代收)。
|
||||
|
||||
---
|
||||
|
||||
## 一、接口背景
|
||||
|
||||
尾款收款模型重构 Epic(方案甲·全实时,不建债表)。旧模型里尾款由司导线下代收,经过「现场垫付 → 报销 → 核单 → 支付完成」长链路后才回写订单已付金额,导致:
|
||||
|
||||
- 核单时「尾款未收齐」一律硬拦,司导已代收但还没走完报销回写的单被卡死;
|
||||
- 财务看不到「这笔钱到底在谁手里」——是客户还欠着,还是司导代收未回款。
|
||||
|
||||
新模型的三条规则:
|
||||
|
||||
1. **金额实时算**:应收/已收/欠收全部实时计算,不落地中间债表;
|
||||
2. **归属实时读主报账人**:订单层有主报账人(reporterRank=PRIMARY)时,未收齐部分视为「主报账人代收中」;没有主报账人时才是「客户欠收」;
|
||||
3. **结清看核单终态**:核单完成(FINALIZED)后由财务域支付完成事件回写已付金额,闭环。
|
||||
|
||||
本次三个接口分别对应:确认行程时把「主报账人」变成前置条件(接口1)、核单欠收硬闸按主报账人豁免(接口2)、财务侧新增应收台账把代收/欠收分列展示(接口3)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 确认行程 | POST | `/v3/admin/order/{orderId}/confirm-itinerary` | 行为变更 + 新错误码 | 确认前必须已有主报账人,否则报 581062;入参出参结构不变 |
|
||||
| 2 | 提交核单(完成核单) | POST | `/v3/admin/order/{orderId}/settlement/finalize` | 行为变更 | 584082 只拦「无主报账人且欠收」的单;有主报账人放行,欠收走代收口径留痕 |
|
||||
| 3 | 财务应收台账分页 | GET | `/admin/finance/receipt/receivable/page` | **新增接口** | 订单维度应收/已收/代收/欠收分页,只读 |
|
||||
|
||||
> 说明:接口2 任务背景里常被称为「核单提交/生成报账」链路,硬闸实际落在「完成核单」写接口上;`GET /v3/admin/order/{orderId}/settlement/reports/reimbursement`(查询主报账人报账表)出参结构**无任何变化**。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 接口1:确认行程 `POST /v3/admin/order/{orderId}/confirm-itinerary`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页点「确认行程」,把订单从「定制中」推进到「待出行」。普通订单在确认弹框中选择本单主报账人(尾款代收人)后提交;团期子订单由团期扇出自动带主报账人,一般无需选择。
|
||||
|
||||
- 认证:管理后台 JWT
|
||||
- 幂等性:非幂等写操作,重复确认会被状态机拦截(订单已不在「定制中」)
|
||||
- 限流:无
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| orderId | Path | Long | 是 | 订单 ID |
|
||||
| reporterAssignmentId | Body | Long | 否 | 新的主报账人人员安排 ID;不传则沿用当前主报账人。**注意:本单当前没有主报账人时,不传会被 581062 拦截** |
|
||||
|
||||
请求体整体可空(`{}` 或不传 body),但仅当订单已有主报账人时才能通过。
|
||||
|
||||
#### 出参 `Result<OrderTransitionRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 是否成功 |
|
||||
| oldStatus | String | 变更前订单状态(确认成功时恒为 `CUSTOMIZING`) |
|
||||
| newStatus | String | 变更后订单状态(确认成功时恒为 `PENDING_DEPARTURE`) |
|
||||
| oldFlowStatus | String | 变更前流程状态 |
|
||||
| newFlowStatus | String | 变更后流程状态 |
|
||||
| triggeredEvents | Array<String> | 本次转换派生的事件列表 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 仅「定制中」订单可确认;5 项前置 checklist 未全过时仍报 581036(既有行为不变)。
|
||||
- 传了 `reporterAssignmentId` 会在确认前先把该人员置为主报账人,再校验主报账人存在性——即「弹框选人」与「确认」是一步完成的。
|
||||
- **团期子订单**:团期人员扇出后订单层已有主报账人副本,不传 `reporterAssignmentId` 也放行;扇出延迟窗口期(极短)可能暂无主报账人被 581062 拦截,稍候重试即可。
|
||||
- `reporterAssignmentId` 传非数字/非法格式报 581046(既有行为不变)。
|
||||
|
||||
#### 示例
|
||||
|
||||
典型成功(确认弹框选了主报账人)见「八、示例」8.1;无主报账人被拦见 8.3。
|
||||
|
||||
---
|
||||
|
||||
### 接口2:提交核单(完成核单)`POST /v3/admin/order/{orderId}/settlement/finalize`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
核单页核对完主报账、单团核算后点「提交核单/完成核单」,冻结核单快照并把订单推进到已核单。
|
||||
|
||||
- 认证:管理后台 JWT
|
||||
- 幂等性:非幂等写操作,重复提交会被核单状态拦截
|
||||
- 限流:无
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| orderId | Path | Long | 是 | 订单 ID(无请求体) |
|
||||
|
||||
#### 出参
|
||||
|
||||
`Result<SettlementSubmitRespVO>`(核单提交结果,结构无变化)。
|
||||
|
||||
#### 业务边界(本次核心变化)
|
||||
|
||||
| 场景 | 变更前 | 变更后 |
|
||||
|------|--------|--------|
|
||||
| 尾款未收齐 + **无主报账人** | 报 584082 拦截 | 报 584082 拦截(不变) |
|
||||
| 尾款未收齐 + **已有主报账人** | 报 584082 拦截 | **放行**,欠收记为主报账人代收口径 |
|
||||
| 尾款已收齐 | 放行 | 放行(不变) |
|
||||
|
||||
- 「放行」不等于「欠收已清」:欠收金额会留在报账快照的欠收勾稽字段里,由财务域下游追款兜底,前端不要把「提交成功」理解为「钱已收齐」。
|
||||
- 代收口径:应代收 = 核单总额 − 客户线上已付(`paidAmount`)。旧的「司机现金代收登记」项已废止,代收不再计入已付金额。
|
||||
- 配套读接口 `GET /v3/admin/order/{orderId}/settlement/reports/reimbursement`(查询主报账人报账表)出参结构无变化。
|
||||
|
||||
#### 示例
|
||||
|
||||
欠收但有主报账人提交成功、无主报账人被 584082 拦截,见「八、示例」8.2 / 8.3。
|
||||
|
||||
---
|
||||
|
||||
### 接口3:财务应收台账分页 `GET /admin/finance/receipt/receivable/page`(新增)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
财务「应收台账」页,按订单维度查看应收/已收/代收/欠收,用于内部财务对账与追款。
|
||||
|
||||
- 认证:管理后台 JWT(网关注入)
|
||||
- 幂等性:只读
|
||||
- 限流:无
|
||||
|
||||
#### 入参(Query)
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| page | Integer | 否 | 1 | 页码 |
|
||||
| pageSize | Integer | 否 | 20 | 每页条数 |
|
||||
| keyword | String | 否 | 空 | 搜索关键字(团号 / 客户姓名 / 产品名 / 订单号 模糊);空=不限 |
|
||||
| receivableStatus | String | 否 | 空 | 收款状态筛选:`UNPAID` / `PARTIAL` / `DONE`;空或非法值=不按状态过滤 |
|
||||
|
||||
#### 出参 `Result<PageResult<ReceiptReceivableRowRespVO>>`
|
||||
|
||||
分页外层固定为 `records` / `total` / `page` / `pageSize`。`records[]` 行字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | String | 订单 ID(字符串防精度丢失) |
|
||||
| orderNo | String | 订单号 |
|
||||
| teamNo | String | 团号 |
|
||||
| productName | String | 产品名 |
|
||||
| customerName | String | 客户姓名 |
|
||||
| customerPhone | String | 客户手机号(**明文**,内部财务对账用) |
|
||||
| orderStatus | String | 订单粗状态枚举值(见「六、枚举」) |
|
||||
| orderStatusName | String | 订单粗状态中文名 |
|
||||
| receivableStatus | String | 收款状态:`UNPAID` 待收款 / `PARTIAL` 部分收款 / `DONE` 已收讫 |
|
||||
| receivableStatusName | String | 收款状态中文名 |
|
||||
| receivableAmount | Number | 应收总额(取消单归 0) |
|
||||
| paidAmount | Number | 已收金额(毛额,退款不回减) |
|
||||
| collectedAmount | Number | 代收金额(主报账人代收中的实时金额;取消单归 0) |
|
||||
| balanceAmount | Number | 欠收金额(未指定主报账人的实时欠收;取消单归 0) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **代收列与欠收列互斥**:同一行 `collectedAmount` 与 `balanceAmount` 必有一列为 0——订单指定了主报账人,未收齐部分进代收列;未指定主报账人,进欠收列。两列合计恒等于该单实时待收尾款。
|
||||
- 收款状态由金额派生:已收=0 → `UNPAID`;已收>0 且仍有欠收/代收 → `PARTIAL`;欠收代收均=0 且已收>0 → `DONE`。
|
||||
- 只读接口,无登记/核销按钮;追款动作不在本接口。
|
||||
- 本接口在 hl-finance 服务(`/admin/finance/*`),与订单域接口不同服务,但同一网关入口。
|
||||
|
||||
#### 示例
|
||||
|
||||
见「八、示例」8.1 / 8.2。
|
||||
|
||||
---
|
||||
|
||||
## 四、接口入参
|
||||
|
||||
各接口入参已分别内联在「三、接口详情」各小节,不再汇总大表。
|
||||
|
||||
## 五、出参字段
|
||||
|
||||
各接口出参已分别内联在「三、接口详情」各小节,不再汇总大表。
|
||||
|
||||
## 六、枚举 / 数据字典
|
||||
|
||||
### 6.1 reporterRank(报账人等级,接口1 相关概念)
|
||||
|
||||
| 值 | 中文名 | 说明 |
|
||||
|----|--------|------|
|
||||
| PRIMARY | 主报账人 | 单内唯一;本次起确认行程前必须存在(尾款代收人) |
|
||||
| SECONDARY | 次报账人 | 单内唯一,不满足接口1 的前置条件 |
|
||||
| NONE | 非报账人 | 默认值 |
|
||||
|
||||
### 6.2 receivableStatus(收款状态,接口3 行字段 + 筛选项)
|
||||
|
||||
| 值 | 中文名 | 派生条件 |
|
||||
|----|--------|----------|
|
||||
| UNPAID | 待收款 | 已收金额 = 0 |
|
||||
| PARTIAL | 部分收款 | 已收 > 0 且(欠收 + 代收)> 0 |
|
||||
| DONE | 已收讫 | 欠收 + 代收 = 0 且已收 > 0 |
|
||||
|
||||
### 6.3 orderStatus(订单粗状态,接口3 行字段)
|
||||
|
||||
| 值 | 中文名 |
|
||||
|----|--------|
|
||||
| PENDING_PAY | 待支付 |
|
||||
| CUSTOMIZING | 定制中 |
|
||||
| PENDING_DEPARTURE | 待出行 |
|
||||
| TRAVELLING | 出行中 |
|
||||
| COMPLETED | 已完成 |
|
||||
| CANCELLED | 已取消 |
|
||||
|
||||
## 七、错误码
|
||||
|
||||
| 错误码 | 报文 | 触发接口 | 说明 |
|
||||
|--------|------|----------|------|
|
||||
| **581062** | 请先指定本单主报账人(尾款代收人) | 接口1 确认行程 | **本次新增**。确认前置换后仍无主报账人时抛出;团期单扇出延迟窗口期被拦属预期,稍候重试 |
|
||||
| 581036 | 确认订单前置校验未通过,请先补全所有必填项 | 接口1 确认行程 | 既有。5 项 checklist 未全过 |
|
||||
| 581046 | 报账人ID格式非法,须为有效的数字ID | 接口1 确认行程 | 既有。`reporterAssignmentId` 格式非法 |
|
||||
| 584082 | 存在待收尾款,请收齐后再提交核单 | 接口2 提交核单 | 既有但**触发条件收紧**:现在仅「无主报账人且欠收不为 0」才抛出;有主报账人的欠收单不再触发 |
|
||||
|
||||
## 八、示例
|
||||
|
||||
### 8.1 典型成功
|
||||
|
||||
**接口1:确认行程(确认弹框选了主报账人)**
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/2086272700140957697/confirm-itinerary
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"reporterAssignmentId": 2072930844657283074
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"oldStatus": "CUSTOMIZING",
|
||||
"newStatus": "PENDING_DEPARTURE",
|
||||
"oldFlowStatus": "CUSTOMIZING",
|
||||
"newFlowStatus": "CONFIRMED",
|
||||
"triggeredEvents": ["CHECKLIST_CONFIRMED"]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**接口3:应收台账分页(指定了主报账人的在途单,欠收进代收列)**
|
||||
|
||||
```http
|
||||
GET /admin/finance/receipt/receivable/page?page=1&pageSize=20&keyword=26-0001
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "2086272700140957697",
|
||||
"orderNo": "HL20260920001",
|
||||
"teamNo": "26-0001",
|
||||
"productName": "呼伦贝尔草原 5 日游",
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800001111",
|
||||
"orderStatus": "TRAVELLING",
|
||||
"orderStatusName": "出行中",
|
||||
"receivableStatus": "PARTIAL",
|
||||
"receivableStatusName": "部分收款",
|
||||
"receivableAmount": 12800.00,
|
||||
"paidAmount": 6400.00,
|
||||
"collectedAmount": 6400.00,
|
||||
"balanceAmount": 0
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 边界情况
|
||||
|
||||
**接口2:尾款未收齐但已有主报账人——变更前被拦、变更后放行**
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/2086272700140957697/settlement/finalize
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "submitted": true },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:提交成功不代表欠收已清,欠收金额留在报账快照勾稽字段中,由财务域下游追款。
|
||||
|
||||
**接口3:未指定主报账人的订单——同一笔未收齐金额进欠收列**
|
||||
|
||||
```json
|
||||
{
|
||||
"receivableStatus": "PARTIAL",
|
||||
"receivableStatusName": "部分收款",
|
||||
"receivableAmount": 12800.00,
|
||||
"paidAmount": 6400.00,
|
||||
"collectedAmount": 0,
|
||||
"balanceAmount": 6400.00
|
||||
}
|
||||
```
|
||||
|
||||
**接口3:空结果(关键词无匹配)**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 业务失败
|
||||
|
||||
**接口1:无主报账人且弹框未选人 → 581062**
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/2086272700140957697/confirm-itinerary
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581062,
|
||||
"message": "请先指定本单主报账人(尾款代收人)",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**接口2:尾款未收齐且无主报账人 → 584082(唯一仍会被拦的情形)**
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/2086272700140957697/settlement/finalize
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 584082,
|
||||
"message": "存在待收尾款,请收齐后再提交核单",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
## 九、业务边界
|
||||
|
||||
- 接口1 仅适用「定制中 → 待出行」确认动作;其他状态变更不受影响。
|
||||
- 接口1 团期子订单正常路径无需前端传参(扇出已带主报账人);扇出延迟窗口被 581062 拦时引导「稍候重试」即可,不要当成数据异常。
|
||||
- 接口2 放行后核单可正常完成,但财务仍会在应收台账/报账勾稽里看到代收欠收金额;「已核单」不等于「已收讫」。
|
||||
- 接口3 为只读台账,不提供任何写操作;`customerPhone` 为明文,仅限内部财务对账场景使用,前端不要在非财务页面引用该接口。
|
||||
- 接口3 已取消订单的金额列全部归 0。
|
||||
|
||||
## 十、修改前后对比
|
||||
|
||||
### 10.1 字段级对比
|
||||
|
||||
| 接口 | 字段 | 变更 |
|
||||
|------|------|------|
|
||||
| 接口1 确认行程 | 入参/出参全部字段 | 无变化(`reporterAssignmentId` 仍为非必填,但语义从「纯可选置换」变为「无主报账人时事实必填」) |
|
||||
| 接口2 提交核单 | 入参/出参全部字段 | 无变化 |
|
||||
| 接口3 应收台账 | 全部字段 | 新增接口,无对比 |
|
||||
|
||||
### 10.2 行为级对比
|
||||
|
||||
| 场景 | 变更前 | 变更后 |
|
||||
|------|--------|--------|
|
||||
| 确认行程时本单无主报账人 | 直接确认成功 | 报 581062,确认不发生;传 `reporterAssignmentId` 选人后放行 |
|
||||
| 核单时尾款未收齐、有主报账人 | 报 584082 拦死 | 放行,欠收记主报账人代收口径 |
|
||||
| 核单时尾款未收齐、无主报账人 | 报 584082 拦死 | 报 584082 拦死(不变) |
|
||||
| 应代收口径 | 核单总额 −(已付 − 司机现金代收登记合计) | 核单总额 − 已付(司机现金代收登记项已废止) |
|
||||
| 财务看尾款归属 | 无接口可看 | 应收台账代收/欠收互斥分列 |
|
||||
|
||||
## 十一、影响评估 / 回滚
|
||||
|
||||
- **破坏性**:接口1 对「此前无主报账人也能确认」的流程是行为收紧,普通订单确认弹框**必须**支持选择主报账人并传 `reporterAssignmentId`,否则确认会被 581062 拦截——**前端需要同步上线**。
|
||||
- 接口2 是放行面扩大,前端对 584082 的既有提示逻辑继续有效(触发面变窄),无强制改动;但原来「被拦 → 引导收尾款」的引导文案对「有主报账人」场景不再出现,如有相关 workaround 可清理。
|
||||
- 接口3 纯新增,不影响存量页面。
|
||||
- **回滚**:后端回滚后,接口1 恢复「无主报账人也可确认」、接口2 恢复「欠收一律拦」、接口3 下线(请求返回 404)。回滚期间前端确认弹框保留选人逻辑无副作用(多传字段旧版兼容)。
|
||||
|
||||
## 十二、注意事项
|
||||
|
||||
- 确认弹框的主报账人候选来自本单人员安排,`reporterAssignmentId` 传人员安排 ID(不是用户 ID、不是员工编号)。
|
||||
- 581062 与 581036 可能先后出现:先补 checklist(581036),再补主报账人(581062),前端引导顺序建议先 checklist 后选人。
|
||||
- 「确认成功」「核单提交成功」都不代表尾款已收齐;尾款是否收讫以应收台账 `receivableStatus=DONE` 为准。
|
||||
- 应收台账的代收/欠收两列互斥,前端渲染时不要对两列同时展示非 0 值做兜底合并——合计即实时待收尾款。
|
||||
- 本 Epic 还包含财务域支付完成后回写订单已付金额的配套链路(PR-5b 起),对管理后台 REST 契约无新增字段,不单独列接口。
|
||||
|
||||
## 十三、关联 / 联系人
|
||||
|
||||
- Issue(主):https://git.1814.love/wx/HL/issues/8248
|
||||
- Issue(确认行程主报账人必填化):https://git.1814.love/wx/HL/issues/8265
|
||||
- Issue(财务应收台账):https://git.1814.love/wx/HL/issues/8191
|
||||
- PR-6a(接口1):https://git.1814.love/wx/HL/pulls/8274 | merge commit:https://git.1814.love/wx/HL/commit/f6b1960a93
|
||||
- PR-5a(接口2):https://git.1814.love/wx/HL/pulls/8263 | merge commit:https://git.1814.love/wx/HL/commit/a480f3dacd
|
||||
- PR-3(接口3):https://git.1814.love/wx/HL/pulls/8207 | merge commit:https://git.1814.love/wx/HL/commit/768136626e
|
||||
- 后端负责人:yst(腰苏图)
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,677 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8268"
|
||||
title: "团期人工「确认」:新增确认端点(配置 → 确认),确认后才出合同保险并全团逐户补发;确认物资前移到配置节点;物料门复判端点下线"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "72b5e17cada78a8f491f9dc1cb6af3d9e42ab457"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "六节点定案(SRS §0.27.3 / §0.27.5 #2~#5):「配置 → 确认」由系统自动推进改为人工点「确认」。新增 POST /v3/admin/order/group-batch/{groupBatchId}/confirm(权限码 group-batch:confirm,授 GROUP_BATCH_MANAGER / ADMIN),门 = 房 / 车 / 导游领队 / 摄影四项 ready 全 true 且物资已确认,不满足返新码 589556 并逐项列出未满足项,状态不是 RESOURCE_PREPARING(含重复确认)返 589501;成功后团期进入 MATERIAL_PREPARING、时间线记 BATCH_CONFIRM(确认),事务提交后系统对全团已确认行程的户逐户补发合同与保险。四项 ready 翻真 / 成团 / 免车不再自动推进状态。连带改动:confirm-material 只在 RESOURCE_PREPARING 可调且不再顺带准入待出发(其它状态 589501);合同保险出具门改为团期已确认(MATERIAL_PREPARING 及之后),确认前手动出具返 589548,文案改为「团期确认后才能出具合同与保险」,合同保险面板 issuable 同口径;POST .../recheck-material-gate 下线(589561 不再抛出)。前端需新增「确认」按钮与 589556 逐项提示、把确认物资按钮挪到配置节点、移除物料门复判入口。 前端已交付并验证:团期详情工具条新增平铺 primary「确认」按钮(仅 RESOURCE_PREPARING,不可撤销二次确认弹窗,589556 逐项清单透 message);确认物资按钮可调状态翻转为仅 RESOURCE_PREPARING,成功文案引导点「确认」;合同保险 issueTip 改「团期确认后才能出具合同与保险」(issuable 仍直读面板);recheck-material-gate 前端零调用零删除。SuppliesPanel 13 例+ChipStaffDisplay 5 例+index 7 例全绿,hl-admin@72b5e17c(+8da805a4 回归锁)。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期状态流转: 人工「确认」取代自动推进,确认后才出合同保险(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186);权限种子在 hl-user-service
|
||||
> **PR**: #8302
|
||||
> **Issue**: #8268
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期详情页的「确认」按钮(新增)、「确认物资」按钮、「合同保险」页签的出具按钮与可出具提示;原「复判物料门」入口下线
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **团期不会再自己从「资源准备中」走到「物料准备中」**。改前四项 ready 齐 + 合同保险出齐时系统自动推进;现在必须由团期管理员点「确认」(新端点)。
|
||||
2. **合同 / 保险在确认前一律不出**。改前资源准备中四项配齐即可出具;现在确认前手动出具被 589548 拒,面板 `issuable=false`。确认后系统自动给全团已确认行程的户逐户补发。
|
||||
3. **确认物资挪到确认之前**。`confirm-material` 以前只在物料准备中可调、还会顺带尝试进入待出发;现在只在资源准备中可调,且只置物资已确认,不改团期状态。
|
||||
4. **`recheck-material-gate` 端点已删除**,调用会得到 404。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
六节点定案里「配置 → 确认」是一个**不可撤销的人工动作**:确认之后四项配置与物资锁定(锁定写口见 #8269 条目),系统随即对客出合同与保险。因此它不能再由系统在 ready 位翻真时悄悄推进,也需要独立于 `group-batch:manage` 的授权。
|
||||
|
||||
| 节点 | 持久态 | 本单之后怎么进入 |
|
||||
|---|---|---|
|
||||
| 配置 | `RESOURCE_PREPARING` | 成团(不变) |
|
||||
| 确认 | `MATERIAL_PREPARING` | **仅**人工调用确认端点 |
|
||||
| 出行·待出发 | `PENDING_DEPARTURE` | 七项硬门(不变) |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/confirm` | 新增接口 | 配置 → 确认;门不满足 589556 逐项回执 |
|
||||
| 2 | 确认团期物资 | POST | `/v3/admin/order/group-batch/{groupBatchId}/confirm-material` | 修改(可调状态 + 行为) | 只在 `RESOURCE_PREPARING` 可调;不再顺带准入待出发 |
|
||||
| 3 | 手工复判物料门 | POST | `/v3/admin/order/group-batch/{groupBatchId}/recheck-material-gate` | 删除接口 | 自动推进已删,复判入口随之下线 |
|
||||
| 4 | 手动开合同 / 保险(GB-ADM-031) | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/issue` | 修改(前置门 + 文案) | 确认前 589548,文案改 |
|
||||
| 5 | 作废重开合同 / 保险(GB-ADM-031) | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/reissue` | 修改(前置门 + 文案) | 同上 |
|
||||
| 6 | 合同保险面板(GB-ADM-030) | GET | `/v3/admin/order/group-batch/{groupBatchId}/contracts` | 修改(出参语义) | `issuable` 改为「团期已确认」 |
|
||||
|
||||
网关无改动(均在既有 `/v3/admin/order/group-batch` 前缀下)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期确认 `POST /v3/admin/order/group-batch/{groupBatchId}/confirm`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「配置」节点的「确认」按钮。房、车、导游领队、摄影四项配齐且物资已确认后,团期管理员点确认,团期进入「确认」节点,配置锁定,系统开始逐户出合同与保险。**没有撤销确认。**
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500 |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Void | 成功返回 null;团期状态已是 `MATERIAL_PREPARING`,重新拉详情即可看到 `stage=CONFIRM` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/confirm HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口无列表出参。确认成功后的逐户补发合同保险是**异步**的:确认接口立即返回 200,补发结果在「合同保险」页签逐户可见;单户出具失败不影响确认结果,也不影响其余户,可在该页签手动开(GB-ADM-031)补出。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
确认门不满足(逐项列出,顺序固定为 房、车、导游领队、摄影、物资):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589556,
|
||||
"message": "团期尚不满足确认条件:车未配齐、物资未确认",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
状态不是「配置」(含已确认后再点一次):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期状态不允许当前操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
无权限(角色未授 `group-batch:confirm`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限码 `group-batch:confirm`,授给 `GROUP_BATCH_MANAGER` 与 `ADMIN`;其余角色 589507。与 `group-batch:manage` 分开授权。
|
||||
- 判定顺序:团期存在(589500)→ 状态为 `RESOURCE_PREPARING`(589501)→ 确认门(589556)→ CAS 推进。任一步拒绝**零写入**。
|
||||
- 589556 的未满足项取值只有五种:`房未配齐`、`车未配齐`、`导游领队未配齐`、`摄影未配齐`、`物资未确认`,以「、」连接。
|
||||
- 不需要导游 / 摄影的团、整团免车的团,其 ready 位已由成团免闸 / 免车写口置真,确认时不会被这几项挡住。
|
||||
- 并发确认只有一个成功,其余 589501;重复确认不会重复推进。
|
||||
- 成功后时间线新增一条 `eventType=BATCH_CONFIRM`、`eventTypeName=确认`,带操作人。
|
||||
- 补发只处理子订单状态为待出发 / 出行中(已确认行程)的户;尚未确认行程的户跳过,等其确认行程时由既有自动出具链路出;已出具的户逐项跳过;已取消户不出。
|
||||
|
||||
---
|
||||
|
||||
### 2. 确认团期物资 `POST /v3/admin/order/group-batch/{groupBatchId}/confirm-material`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「物资」页签的「确认物资」按钮。本次起它是团期确认的前置条件之一,在「配置」节点点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500 |
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Void | 成功返回 null;团期状态**不变**,物资已确认标记置真 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/confirm-material HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参。确认前物资可反复增删改、反复确认,每次确认都在时间线记一条「确认物资」流水(内容「确认物资清单」)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不在 `RESOURCE_PREPARING`(含招募中、已确认及之后):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期状态不允许当前操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限码 `group-batch:manage`(不变)。
|
||||
- **只在 `RESOURCE_PREPARING` 可调**;改前只在 `MATERIAL_PREPARING` 可调,两者正好相反。
|
||||
- **不再推进团期状态**:改前确认物资后会顺带尝试进入待出发;现在进入待出发改由子订单确认、定时扫描与「复判待出发硬门」端点负责,七项硬门本身不变。
|
||||
- 取消成团会把物资已确认标记重置(不变)。
|
||||
|
||||
---
|
||||
|
||||
### 3. 手工复判物料门 `POST /v3/admin/order/group-batch/{groupBatchId}/recheck-material-gate`
|
||||
|
||||
**VO**: `GroupBatchMaterialGateRespVO`(已删除)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
**已下线。** 该端点原用于在「资源准备中 → 物料准备中」自动推进卡住时手工复判。自动推进已删除,这一跳只剩人工确认一条路,复判入口不再有意义。请改用新端点 `POST .../confirm`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 端点已删除 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| advanced | Boolean | 已删除(原:是否推进了团期) |
|
||||
| currentStatus / currentStatusName | String | 已删除 |
|
||||
| blockedGate / blockedOrderId / blockedReason | String / Long / String | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/recheck-material-gate HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
路由已不存在,经网关调用返回业务信封 `code=404`(TEST 2026-09-24 实测):
|
||||
|
||||
```json
|
||||
{ "code": 404, "msg": "接口不存在: POST /v3/admin/order/group-batch/2102932276390383618/recheck-material-gate" }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无。端点不存在,不会返回任何业务数据。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
任何调用都返回 `code=404`「接口不存在」(同上):
|
||||
|
||||
```json
|
||||
{ "code": 404, "msg": "接口不存在: POST /v3/admin/order/group-batch/2102932276390383618/recheck-material-gate" }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 原错误码 589561「团期当前状态为「{0}」,不在「资源准备中」,无需复判物料门」不再抛出,仅保留占位防号段复用。
|
||||
- 前端所有调用点与按钮需移除;替代动作是「确认」。
|
||||
|
||||
---
|
||||
|
||||
### 4. 手动开合同 / 保险 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/issue`
|
||||
|
||||
**VO**: `GroupBatchContractIssueReqVO` → `GroupBatchIssueResultVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「合同保险」页签逐户开合同 / 保险。本次只改前置门与拒绝文案,入参出参结构不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | - |
|
||||
| orderIds | Body | List<Long> | 否 | - | 为空 = 本期全部尚未出具的户 |
|
||||
| target | Body | String | ✅ | `CONTRACT` / `INSURANCE` / `BOTH` | 出具目标 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| totalCount | Integer | 本次处理户数 |
|
||||
| successCount | Integer | 成功户数 |
|
||||
| failCount | Integer | 失败户数 |
|
||||
| skipCount | Integer | 跳过户数(已出具 / 已签等) |
|
||||
| results[].orderId | String | 子订单 ID |
|
||||
| results[].target | String | `CONTRACT` / `INSURANCE` |
|
||||
| results[].outcome | String | `SUCCESS` / `SKIPPED` / `FAILED` |
|
||||
| results[].message | String | 失败或跳过原因 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "orderIds": ["770145"], "target": "CONTRACT" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"totalCount": 1,
|
||||
"successCount": 1,
|
||||
"failCount": 0,
|
||||
"skipCount": 0,
|
||||
"results": [
|
||||
{ "orderId": "770145", "target": "CONTRACT", "outcome": "SUCCESS", "message": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
已出具的户逐项 `SKIPPED`,不算失败;单户失败只记在 `results` 里,不影响其余户(不变)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "totalCount": 1, "successCount": 0, "failCount": 0, "skipCount": 1, "results": [ { "orderId": "770145", "target": "CONTRACT", "outcome": "SKIPPED", "message": "该户合同已出具,自动跳过" } ] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期尚未确认(`RECRUITING` / `RESOURCE_PREPARING`,即便四项已配齐):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589548,
|
||||
"message": "团期确认后才能出具合同与保险",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限码 `group-batch:contract:issue`(不变)。
|
||||
- 可出具的团期状态:`MATERIAL_PREPARING` / `PENDING_DEPARTURE` / `TRAVELLING` / `REVIEWING` / `SETTLED`;其余一律 589548,整单拒绝、零写入。
|
||||
- 改前 `RESOURCE_PREPARING` 且四项 ready 全真时可出具,本次起不可。
|
||||
|
||||
---
|
||||
|
||||
### 5. 作废重开合同 / 保险 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/reissue`
|
||||
|
||||
**VO**: `GroupBatchContractIssueReqVO` → `GroupBatchIssueResultVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「开错」态的补救通路:先作废该户现有单据再重开。前置门与手动开完全一致。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | - |
|
||||
| orderIds | Body | List<Long> | 否 | - | 为空 = 本期全部户 |
|
||||
| target | Body | String | ✅ | `CONTRACT` / `INSURANCE` / `BOTH` | 重开目标 |
|
||||
| reason | Body | String | 否 | - | 作废原因 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| totalCount / successCount / failCount / skipCount | Integer | 同手动开 |
|
||||
| results[] | List | 逐户结果,同手动开 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "orderIds": ["770145"], "target": "CONTRACT", "reason": "方案选错" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"totalCount": 1,
|
||||
"successCount": 1,
|
||||
"failCount": 0,
|
||||
"skipCount": 0,
|
||||
"results": [
|
||||
{ "orderId": "770145", "target": "CONTRACT", "outcome": "SUCCESS", "message": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
作废失败的户直接记 `FAILED`,不进重开,不影响其他户(不变)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "totalCount": 1, "successCount": 0, "failCount": 1, "skipCount": 0, "results": [ { "orderId": "770145", "target": "CONTRACT", "outcome": "FAILED", "message": "作废失败" } ] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589548,
|
||||
"message": "团期确认后才能出具合同与保险",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 与手动开共用同一出具门与同一文案。
|
||||
- 确认前没有任何已出具的单据可重开,调用即 589548。
|
||||
|
||||
---
|
||||
|
||||
### 6. 合同保险面板 `GET /v3/admin/order/group-batch/{groupBatchId}/contracts`
|
||||
|
||||
**VO**: `GroupBatchContractBoardVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「合同保险」页签首屏:顶部三格统计 + 逐户卡片;`issuable` 决定出具按钮是否可点。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | - |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| issuable | Boolean | **语义改变**:= 团期已人工确认(`MATERIAL_PREPARING` 及之后的可出具状态);改前 = 四项配齐 |
|
||||
| batchStatus | String | 团期状态存储值,`issuable=false` 时可据此提示(不变) |
|
||||
| totalCount / contractIssuedCount / contractSignedCount / insuranceIssuedCount | Integer | 不变 |
|
||||
| items | List | 逐户明细(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985/contracts HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"issuable": false,
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"totalCount": 3,
|
||||
"contractIssuedCount": 0,
|
||||
"contractSignedCount": 0,
|
||||
"insuranceIssuedCount": 0,
|
||||
"items": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无活跃子订单时 `totalCount=0`、`items` 为空数组(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "issuable": false, "batchStatus": "RESOURCE_PREPARING", "totalCount": 0, "items": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `RESOURCE_PREPARING` 且四项已配齐时,改前 `issuable=true`,现在 `false`;提示文案建议为「团期确认后才能出具合同与保险」。
|
||||
- 其余字段口径不变。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用顺序
|
||||
|
||||
| 场景 | 调用 |
|
||||
|------|------|
|
||||
| ✅ 配置节点收尾 | 配房 / 车 / 导摄 → `confirm-material` → `confirm` |
|
||||
| ❌ 先确认团期再确认物资 | `confirm` → 589556「…物资未确认」;确认后再调 `confirm-material` → 589501 |
|
||||
| ❌ 确认前出具合同 | `contracts/issue`(`RESOURCE_PREPARING`)→ 589548 |
|
||||
| ❌ 继续调复判物料门 | `recheck-material-gate` → 404 |
|
||||
|
||||
### 589556 的处理
|
||||
|
||||
message 的冒号之后就是未满足项清单,可直接展示给操作人;不要再去调已下线的复判端点找原因。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端动作 | 外部可观察的写入 |
|
||||
|----------|------------------|
|
||||
| `confirm` 成功 | 团期状态 `RESOURCE_PREPARING → MATERIAL_PREPARING`(CAS,一次)+ 时间线一条「确认」(含操作人);提交后异步逐户生成合同 / 保单 |
|
||||
| `confirm` 被拒(589501 / 589556 / 589507) | 零写入 |
|
||||
| `confirm-material` 成功 | 物资已确认标记置真 + 时间线一条「确认物资」;团期状态不变 |
|
||||
| `confirm-material` 被拒 | 零写入 |
|
||||
| `contracts/issue` / `reissue` 被 589548 拒 | 零写入 |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 团期不存在 → 589500。
|
||||
- 四项 ready 翻真、成团、整团免车**都不会再推进团期状态**;团期会停在「配置」直到有人点确认。
|
||||
- 确认后补发是异步的,确认接口不等补发完成;补发整轮失败(如团期被并发流团)只影响出具,不回滚确认,可在「合同保险」页签手动补出。
|
||||
- 进入待出发的七项硬门不变。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### 589556 未满足项(GroupBatchService.unmetConfirmConditions)
|
||||
|
||||
**所属字段**: 错误 `message` 冒号后的清单 | **类型**: `String`(「、」分隔)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `房未配齐` | 房 | 团期配房完成标志为假 |
|
||||
| `车未配齐` | 车 | 团期配车完成标志为假(整团免车时为真) |
|
||||
| `导游领队未配齐` | 导游领队 | 不需要导游的团成团时已置真 |
|
||||
| `摄影未配齐` | 摄影 | 不需要摄影的团成团时已置真 |
|
||||
| `物资未确认` | 物资 | 未调用 `confirm-material` |
|
||||
|
||||
### 时间线事件(GroupBatchLogEventType,新增一值)
|
||||
|
||||
**所属字段**: 团期状态流水 `eventType` / `eventTypeName` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `BATCH_CONFIRM` | 确认 | 本次新增;人工确认时写入,`fromStatus=RESOURCE_PREPARING`、`toStatus=MATERIAL_PREPARING` |
|
||||
| `BATCH_RESOURCE_READY` | 资源就绪·进物资准备 | 原自动推进事件,本次起不再写入;历史行照常显示 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 合同保险面板 `issuable` | 四项配齐(或已进物料准备中)即 true | 团期已确认(物料准备中及之后)才 true |
|
||||
| 589548 message | 房/车/导/摄四项配齐后才能出具合同与保险 | 团期确认后才能出具合同与保险 |
|
||||
| 589556 | 无 | 新增:团期尚不满足确认条件:{0} |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 资源准备中 → 物料准备中 | 四项 ready + 逐户合同已签、保险已出 → 系统自动推进 | 只能人工 `confirm`,门 = 四项 ready + 物资已确认 |
|
||||
| 确认物资可调状态 | `MATERIAL_PREPARING` | `RESOURCE_PREPARING` |
|
||||
| 确认物资的副作用 | 顺带尝试进入待出发 | 不改团期状态 |
|
||||
| 合同保险出具时机 | 资源准备中四项配齐即可 | 确认后;确认时对已确认行程的户自动补发 |
|
||||
| 复判物料门端点 | 可用 | 删除(404) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是。复判端点删除;确认物资的可调状态翻转;确认前不能出合同保险。
|
||||
- **前端是否必须同步上线**: 是。缺少「确认」按钮时,新成团的团期会一直停在「配置」节点。
|
||||
- **前端 workaround 清理点**: 移除「复判物料门」入口及其结果弹窗;「确认物资」按钮的显示条件由物料准备中改为资源准备中;合同保险页签按 `issuable` 置灰的提示文案改为「团期确认后才能出具合同与保险」。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期「配置 → 确认」这一跳、确认物资、合同保险出具门。
|
||||
- **零影响**:
|
||||
- 成团、取消成团、流团的入参与出参
|
||||
- 进入待出发的七项硬门与「复判待出发硬门」端点
|
||||
- 子订单确认行程后的逐户自动出具链路(团期已确认时照常出)
|
||||
- 小程序端
|
||||
- 另:内部定时任务端点 `/v3/internal/jobs/group-batch-material-gate/run` 与其定时任务同批下线,属服务间内部接口,管理后台不调用。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-user-service + hl-order-service-v3 = dev-v3 @ d9fdd7fe0(2026-09-24 09:18 / 09:20),经网关 `https://api.test.1814.love` 真实鉴权实测(2026-09-24 09:22–09:45);工单 #8268 已验收关单。
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 四项 ready + 物资已确认,GROUP_BATCH_MANAGER 调 `POST /confirm` | 200,RESOURCE_PREPARING → MATERIAL_PREPARING,时间线 `BATCH_CONFIRM`(含操作人) |
|
||||
| 2 | 门不满足调 confirm | 589556「团期尚不满足确认条件:房未配齐、车未配齐、摄影未配齐、物资未确认」,状态不变 |
|
||||
| 3 | 招募中 / 已确认后重复调 confirm | 589501,不推进 |
|
||||
| 4 | ROOM_MANAGER / VEHICLE_MANAGER / CUSTOMIZER / FINANCE 调 confirm | 589507;Flyway `20260923.268` 已执行,仅授 ADMIN 与 GROUP_BATCH_MANAGER |
|
||||
| 5 | 成团及四项依次翻真 | 不再自动推进,停在 RESOURCE_PREPARING 直到人工确认 |
|
||||
| 6 | 确认前户确认行程 / 手动出具 | 不自动出具;`GET /contracts` 的 `issuable=false`;手动出具 589548「团期确认后才能出具合同与保险」 |
|
||||
| 7 | 确认后全团补发 | `total=6 success=4 skipped=2 failed=0`:已确认行程两户合同 GENERATED + 保险 INSURED;已取消户、未确认行程户不出;再次出具返回 SKIPPED |
|
||||
| 8 | 配置阶段 / 确认后调 `confirm-material` | 200(`material_confirmed=1`)/ 589501 |
|
||||
| 9 | 旧 `recheck-material-gate` 与内部 job 端点 | `code=404`「接口不存在」;sys_job 与 QRTZ 均无 1044 |
|
||||
| 10 | 合同签齐 + 七门满足后复检 | 进入 PENDING_DEPARTURE(签署以 SQL 模拟) |
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| — | #7105 | 合同保险出具门(四项配齐,589548) | ❌ 门条件被本单替换,码值保留 |
|
||||
| — | #7526 | 物料门兜底复判(端点 + 定时任务) | ❌ 本单下线 |
|
||||
| — | #7528 | 确认物资后顺带准入待出发 | ❌ 本单删除该顺带准入 |
|
||||
| **本 PR #8302** | **#8268** | 人工确认 + 确认后出合同保险 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8268](https://git.1814.love/wx/HL/issues/8268)
|
||||
- 关联 PR: [wx/HL#8302](https://git.1814.love/wx/HL/pulls/8302)
|
||||
- 同批六节点条目:确认后锁定配置(#8269)、六节点展示与看板七桶(#8271)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8268](https://git.1814.love/wx/HL/issues/8268)
|
||||
- **PR**: [#8302](https://git.1814.love/wx/HL/pulls/8302)
|
||||
- **Merge commit**: [d9fdd7fe0](https://git.1814.love/wx/HL/commit/d9fdd7fe038952ac3ffdd31913dfcc4a2fc40574)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,800 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8271"
|
||||
title: "团期看板与详情按六节点展示:分页 / 详情 / 看板行新增 stage 与出行子状态,统计条与 opsStage 改七桶(旧八桶过渡期兼容),核团 / 验团用语统一为核单 / 结算"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "3fa07c47a0c36277508c383979e8542c10149a55"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "六节点定案(SRS §0.27.1 / §0.27.4):持久九态 batchStatus 不动,服务端派生节点 stage(RECRUIT 招募 / CONFIGURE 配置 / CONFIRM 确认 / TRIP 出行 / REVIEW 核单 / SETTLE 结算 / DISBANDED 已流团)与出行子状态 tripSubStatus(待出发 / 出行中 / 已返团),只供展示。团期分页、详情、看板行新增 stage / stageName / tripSubStatus / tripSubStatusName 四字段;既有字段 opsStage 的取值由八桶改为七桶(与 stage 逐字同值);统计条 buckets 固定 13 键 = 七桶 + 6 个旧八桶别名键(别名键不计入 total);opsStage 筛选接受七桶,旧八桶 code 过渡期按原口径兼容;导出 CSV「状态」列改为节点名。PR #8303 补充:面向用户的核团 / 验团用语统一为核单 / 结算(6 个错误码文案、时间线事件中文标签、核单状态 CHECKED 中文名),码值与存储值不变。前端需把看板页签与统计条切到七桶、详情页按 stage / tripSubStatus 展示节点;旧八桶别名在前端切换完成前保留。 前端已交付(3fa07c47):看板页签与统计条切七桶、stageMetaOf 折叠出行复合桶并拼 tripSubStatusName、详情 6 段步骤条、核团/验团用语统一核单/结算;batch 域 403 例全绿。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期看板: 按六节点展示,统计条与筛选改七桶(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8276(代码)、#8279(文档)、#8303(核单 / 结算用语)
|
||||
> **Issue**: #8271
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期看板(页签、统计条、列表行、导出)、团期详情头部节点展示、团期时间线与核单 / 结算相关报错文案
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **`opsStage` 响应取值变了**:以前是八桶(`FORMED` / `PENDING_TRIP` / `TRAVELLING` / `TRIP_FINISHED` / `AUDITING` / `CHECKED` …),现在只输出七桶(`CONFIGURE` / `CONFIRM` / `TRIP` / `REVIEW` / `SETTLE` …),且与新字段 `stage` 逐字同值。按旧值做过 `switch` 的前端代码会落到默认分支。
|
||||
2. **原「已成团」一桶拆成两桶**:`RESOURCE_PREPARING` → 配置(`CONFIGURE`),`MATERIAL_PREPARING` → 确认(`CONFIRM`);原「待出行 / 出行中 / 出行完毕」三桶合成一个「出行」(`TRIP`),细分看 `tripSubStatus`。
|
||||
3. **统计条 `buckets` 从 8 键变 13 键**:七桶在前、6 个旧别名键在后。`total` 只等于七桶之和,**不要再对 `buckets` 整体求和**(会重复计数)。
|
||||
4. **旧入参仍然能用**:页签继续传旧八桶 code 给 `opsStage` 筛选,结果与改前逐条一致(按原口径展开);统计条的旧键名计数也照旧给。
|
||||
5. **用语**:「核团中 / 已验团」改为「核单 / 结算」,涉及 6 个错误码文案、时间线事件中文名、核单状态 `CHECKED` 的中文名;码值与存储值一律不变。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期六节点定案:看板与详情按「招募 → 配置 → 确认 → 出行 → 核单 → 结算」展示,另有分叉终态「已流团」。持久九态 `batchStatus` 不动、不新增持久列,节点与出行子状态由服务端唯一派生,**只供展示,不承担业务判断**——按钮可用性、权限仍以 `batchStatus` 与各就绪位为准。
|
||||
|
||||
| 节点 `stage` | `stageName` | 覆盖的 `batchStatus` | 说明 |
|
||||
|---|---|---|---|
|
||||
| `RECRUIT` | 招募 | `RECRUITING` | 未建团行(产品侧有班期、订单侧尚无团期)也恒为 `RECRUIT` |
|
||||
| `CONFIGURE` | 配置 | `RESOURCE_PREPARING` | 配房 / 车 / 导游领队 / 摄影与物资,确认前可改 |
|
||||
| `CONFIRM` | 确认 | `MATERIAL_PREPARING` | 人工确认后配置锁定,逐户出合同与保险 |
|
||||
| `TRIP` | 出行 | `PENDING_DEPARTURE`、`TRAVELLING`、`TRIP_FINISHED` | 复合节点,细分见 `tripSubStatus` |
|
||||
| `REVIEW` | 核单 | `REVIEWING` | 原「核团中」 |
|
||||
| `SETTLE` | 结算 | `SETTLED` | 原「已验团」 |
|
||||
| `DISBANDED` | 已流团 | `CANCELLED` | 分叉终态 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期分页列表(GB-ADM-001) | GET | `/v3/admin/order/group-batch` | 出参新增字段 + 出参取值变化 + 入参取值扩展 | 新增四字段;`opsStage` 出参改七桶;`opsStage` 筛选接受七桶并兼容旧八桶 |
|
||||
| 2 | 团期详情(GB-ADM-002) | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 出参新增字段 + 出参取值变化 | 新增四字段;`opsStage` 改七桶 |
|
||||
| 3 | 团期看板列表 | GET | `/v3/admin/order/group-batch/board` | 出参新增字段 + 出参取值变化 | 命中 / 未命中 / 孤儿三类行均带四字段;`opsStage` 改七桶 |
|
||||
| 4 | 团期看板统计条(GB-ADM-009) | GET | `/v3/admin/order/group-batch/summary` | 出参取值变化 | `buckets` 13 键;`total` = 七桶之和 |
|
||||
| 5 | 导出团期列表 CSV(GB-ADM-008) | GET | `/v3/admin/order/group-batch/export` | 入参取值扩展 + 导出内容变化 | `opsStage` 同分页口径;「状态」列改为节点名 |
|
||||
| 6 | 团期状态流水(GB-ADM-096) | GET | `/v3/admin/order/group-batch/{groupBatchId}/status-logs` | 出参取值变化(文案) | 5 个核团 / 验团事件的 `eventTypeName` 改为核单 / 结算用语 |
|
||||
|
||||
路径、HTTP 方法、权限码、信封结构均不变;网关无改动。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期分页列表 `GET /v3/admin/order/group-batch`
|
||||
|
||||
**VO**: `GroupBatchListReqVO` → `PageResult<GroupBatchPageItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期看板主列表(按页签筛选)。页签既可以传七桶 code,也可以继续传旧八桶 code。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| opsStage | Query | String | 否 | 七桶或旧八桶 code;空白 / 非法值忽略 | **本次改取值**:七桶 `RECRUIT` / `CONFIGURE` / `CONFIRM` / `TRIP` / `REVIEW` / `SETTLE` / `DISBANDED`;旧八桶 `FORMED` / `PENDING_TRIP` / `TRAVELLING` / `TRIP_FINISHED` / `AUDITING` / `CHECKED` 过渡期仍按原口径展开 |
|
||||
| scope | Query | String | 否 | `ONGOING` / `FINISHED` / `ALL` | 班期范围;与 `opsStage` 取交集(不变) |
|
||||
| productId | Query | Long | 否 | - | 按产品筛选(不变) |
|
||||
| batchStatus | Query | String | 否 | 九态 code | 精确九态筛选(不变) |
|
||||
| month | Query | String | 否 | `yyyy-MM` | 出发月份(不变) |
|
||||
| keyword | Query | String | 否 | - | 班期编号 / 名称模糊(不变) |
|
||||
| pageNo | Query | Integer | 否 | 默认 1 | 页码(不变) |
|
||||
| pageSize | Query | Integer | 否 | 默认 20,最大 100 | 每页条数(不变) |
|
||||
|
||||
其余既有筛选参数(`deadlineFrom` / `deadlineTo` / `departFrom` / `departTo` / `sortBy` / `sortOrder`)不变。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records[].batchStatus | String | 持久九态,**业务判断用这个**(不变) |
|
||||
| records[].batchStatusName | String | 九态中文名(不变,如「资源准备中」「出行完毕」) |
|
||||
| records[].stage | String | **新增**。节点 code,取值见「一、背景」七个 |
|
||||
| records[].stageName | String | **新增**。节点中文名,与 `stage` 同生同灭 |
|
||||
| records[].tripSubStatus | String | **新增**。出行子状态 `PENDING_DEPARTURE` / `TRAVELLING` / `TRIP_FINISHED`;仅 `stage = TRIP` 时有值,其余为 null |
|
||||
| records[].tripSubStatusName | String | **新增**。待出发 / 出行中 / 已返团;与 `tripSubStatus` 同生同灭 |
|
||||
| records[].opsStage | String | **取值改变**:与 `stage` 逐字同值(七桶),旧八桶值不再输出 |
|
||||
| records[].opsStageName | String | **取值改变**:与 `stageName` 同值 |
|
||||
| total | Long | 命中总数(不变) |
|
||||
|
||||
其余分页项字段不变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch?opsStage=TRIP&scope=ALL&pageNo=1&pageSize=20 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"total": 1,
|
||||
"records": [
|
||||
{
|
||||
"groupBatchId": "2099750660965584898",
|
||||
"batchNo": "GB26100101",
|
||||
"batchStatus": "PENDING_DEPARTURE",
|
||||
"batchStatusName": "待出发",
|
||||
"stage": "TRIP",
|
||||
"stageName": "出行",
|
||||
"tripSubStatus": "PENDING_DEPARTURE",
|
||||
"tripSubStatusName": "待出发",
|
||||
"opsStage": "TRIP",
|
||||
"opsStageName": "出行"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中返回空页;`batchStatus` 为 null 或不在九态内的历史脏数据行,四个新字段与 `opsStage` / `opsStageName` 同为 null,不抛错:
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "total": 0, "records": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
`opsStage` 传非法值不报错(忽略该筛选并记 warn);本接口错误形态沿用既有,如无列表权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 七桶与旧别名 code 不相交,先按七桶解析、未命中再按别名解析。
|
||||
- 旧别名按**原口径**展开,不放大为新节点:`PENDING_TRIP` 仍只筛待出发,`FORMED` = 配置 + 确认。
|
||||
- 服务端严格取 `scope ∩ opsStage`:`REVIEW` / `SETTLE` 及 `TRIP` 里已返团的那部分返团日必然已过,默认 `ONGOING` 会过滤掉,点这些页签需把 `scope` 切到 `ALL`。
|
||||
- 四个新字段是纯内存映射,整页无额外查询。
|
||||
|
||||
---
|
||||
|
||||
### 2. 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}`
|
||||
|
||||
**VO**: `GroupBatchDetailRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页头部展示当前节点(如「出行 · 待出发」)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| batchStatus | String | 持久九态(不变) |
|
||||
| batchStatusName | String | 九态中文名(不变) |
|
||||
| stage | String | **新增**。节点 code |
|
||||
| stageName | String | **新增**。节点中文名 |
|
||||
| tripSubStatus | String | **新增**。仅 `stage = TRIP` 时有值 |
|
||||
| tripSubStatusName | String | **新增**。与 `tripSubStatus` 同生同灭 |
|
||||
| opsStage | String | **取值改变**:七桶,与 `stage` 同值 |
|
||||
| opsStageName | String | **取值改变**:与 `stageName` 同值 |
|
||||
|
||||
其余详情字段不变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchStatus": "MATERIAL_PREPARING",
|
||||
"batchStatusName": "物料准备中",
|
||||
"stage": "CONFIRM",
|
||||
"stageName": "确认",
|
||||
"tripSubStatus": null,
|
||||
"tripSubStatusName": null,
|
||||
"opsStage": "CONFIRM",
|
||||
"opsStageName": "确认"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
非出行节点 `tripSubStatus` / `tripSubStatusName` 为 null;脏数据行四字段同为 null:
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "batchStatus": null, "stage": null, "stageName": null, "tripSubStatus": null, "tripSubStatusName": null, "opsStage": null, "opsStageName": null }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 子状态中文名「已返团」与九态 `batchStatusName`「出行完毕」不同名,两套文案各自展示,不要互相替换。
|
||||
- 前端不得用 `stage` / `tripSubStatus` 判断按钮可用性或权限。
|
||||
|
||||
---
|
||||
|
||||
### 3. 团期看板列表 `GET /v3/admin/order/group-batch/board`
|
||||
|
||||
**VO**: `List<GroupBatchBoardItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
按产品展示全部班期(含未建团行与孤儿行)的看板卡片列表。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productId | Query | Long | ✅ | - | 产品 ID(不变) |
|
||||
| scope | Query | String | 否 | `ONGOING` / `FINISHED` / `ALL`,缺省 `ALL` | 班期范围(不变) |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| [].batchStatus | String | 持久九态(不变);未建团行按 `RECRUITING` |
|
||||
| [].stage | String | **新增**。节点 code;未建团行恒 `RECRUIT` |
|
||||
| [].stageName | String | **新增**。节点中文名 |
|
||||
| [].tripSubStatus | String | **新增**。仅 `stage = TRIP` 时有值 |
|
||||
| [].tripSubStatusName | String | **新增** |
|
||||
| [].opsStage | String | **取值改变**:七桶,与 `stage` 同值 |
|
||||
| [].opsStageName | String | **取值改变**:与 `stageName` 同值 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/board?productId=100001&scope=ALL HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchStatus": "TRAVELLING",
|
||||
"stage": "TRIP",
|
||||
"stageName": "出行",
|
||||
"tripSubStatus": "TRAVELLING",
|
||||
"tripSubStatusName": "出行中",
|
||||
"opsStage": "TRIP",
|
||||
"opsStageName": "出行"
|
||||
},
|
||||
{
|
||||
"productBatchId": "2097250420299530242",
|
||||
"batchStatus": "RECRUITING",
|
||||
"stage": "RECRUIT",
|
||||
"stageName": "招募",
|
||||
"tripSubStatus": null,
|
||||
"tripSubStatusName": null,
|
||||
"opsStage": "RECRUIT",
|
||||
"opsStageName": "招募"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
产品无班期时返回空数组:
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": [], "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 命中 / 未命中 / 孤儿三类行统一带四个新字段。
|
||||
- 行集合、排序、其余字段均不变。
|
||||
|
||||
---
|
||||
|
||||
### 4. 团期看板统计条 `GET /v3/admin/order/group-batch/summary`
|
||||
|
||||
**VO**: `GroupBatchSummaryVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板顶部各页签的计数。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productId | Query | Long | 否 | - | 不变 |
|
||||
| month | Query | String | 否 | `yyyy-MM` | 不变 |
|
||||
| keyword | Query | String | 否 | - | 不变 |
|
||||
| scope | Query | String | 否 | `ONGOING` / `FINISHED` / `ALL` | 不变 |
|
||||
|
||||
本接口不接受 `opsStage`(它的作用正是给出各桶数量)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| total | Integer | **口径改变**:= 七桶之和(旧别名键不计入) |
|
||||
| buckets | Map<String, Integer> | **键集合改变**:固定 13 键,无命中为 0;前 7 键为七桶,后 6 键为旧八桶别名 |
|
||||
| subOrderCount | Integer | 命中团期的活跃子订单合计(不变) |
|
||||
| effectiveScope | String | 实际生效的班期范围(不变) |
|
||||
| filteredOutCount | Integer | 被范围过滤掉的团期数(不变) |
|
||||
|
||||
`buckets` 13 键口径:
|
||||
|
||||
| 键 | 计数口径 | 计入 `total` |
|
||||
|---|---|---|
|
||||
| `RECRUIT` / `CONFIGURE` / `CONFIRM` / `TRIP` / `REVIEW` / `SETTLE` / `DISBANDED` | 七桶;`TRIP` = 待出发 + 出行中 + 已返团 | ✅ |
|
||||
| `FORMED` | 旧别名 = `RESOURCE_PREPARING` + `MATERIAL_PREPARING` | ❌ |
|
||||
| `PENDING_TRIP` | 旧别名 = `PENDING_DEPARTURE` | ❌ |
|
||||
| `TRAVELLING` | 旧别名 = `TRAVELLING` | ❌ |
|
||||
| `TRIP_FINISHED` | 旧别名 = `TRIP_FINISHED` | ❌ |
|
||||
| `AUDITING` | 旧别名 = `REVIEWING` | ❌ |
|
||||
| `CHECKED` | 旧别名 = `SETTLED` | ❌ |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/summary?scope=ALL HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"total": 12,
|
||||
"buckets": {
|
||||
"RECRUIT": 3,
|
||||
"CONFIGURE": 2,
|
||||
"CONFIRM": 1,
|
||||
"TRIP": 4,
|
||||
"REVIEW": 1,
|
||||
"SETTLE": 0,
|
||||
"DISBANDED": 1,
|
||||
"FORMED": 3,
|
||||
"PENDING_TRIP": 2,
|
||||
"TRAVELLING": 1,
|
||||
"TRIP_FINISHED": 1,
|
||||
"AUDITING": 1,
|
||||
"CHECKED": 0
|
||||
},
|
||||
"subOrderCount": 57,
|
||||
"effectiveScope": "ALL",
|
||||
"filteredOutCount": 0
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时 13 键全部为 0:
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "total": 0, "buckets": { "RECRUIT": 0, "CONFIGURE": 0, "CONFIRM": 0, "TRIP": 0, "REVIEW": 0, "SETTLE": 0, "DISBANDED": 0, "FORMED": 0, "PENDING_TRIP": 0, "TRAVELLING": 0, "TRIP_FINISHED": 0, "AUDITING": 0, "CHECKED": 0 }, "subOrderCount": 0 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 按键名读取,不要依赖 Map 下标;也不要对 `buckets` 整体求和。
|
||||
- `RECRUIT` / `DISBANDED` 新旧同名同义,不重复出现。
|
||||
- 状态不在九态内的行被排除,不计入任何桶。
|
||||
|
||||
---
|
||||
|
||||
### 5. 导出团期列表 CSV `GET /v3/admin/order/group-batch/export`
|
||||
|
||||
**VO**: `text/csv` 附件(非 `Result` 信封)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板「导出」按钮,按当前筛选导出 CSV。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| opsStage | Query | String | 否 | 同分页口径 | **本次改取值**:七桶或旧八桶 code |
|
||||
| productId | Query | Long | 否 | - | 不变 |
|
||||
| month | Query | String | 否 | `yyyy-MM` | 不变 |
|
||||
| keyword | Query | String | 否 | - | 不变 |
|
||||
| scope | Query | String | 否 | `ONGOING` / `FINISHED` / `ALL` | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 状态(CSV 第 8 列) | String | **内容改变**:由旧八桶中文名改为节点名;出行节点拼子状态,如「出行·待出发」「出行·出行中」「出行·已返团」 |
|
||||
|
||||
列名、列序(固定 10 列)与单次 2000 行上限不变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/export?opsStage=CONFIGURE&scope=ALL HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
响应为 CSV 附件;「状态」列示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"header": "团期号,期号,日期,出团日,满团名额,已售,剩余,状态,子订单数,整团应收",
|
||||
"状态列取值示例": ["招募", "配置", "确认", "出行·待出发", "出行·出行中", "出行·已返团", "核单", "结算", "已流团"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时只输出表头一行(不变):
|
||||
|
||||
```json
|
||||
{ "header": "团期号,期号,日期,出团日,满团名额,已售,剩余,状态,子订单数,整团应收", "rows": 0 }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 按「状态」列中文做过解析的下游需同步:旧值「已成团 / 待出行 / 核团中 / 已验团」不再出现。
|
||||
- 筛选口径与分页接口共用同一展开逻辑。
|
||||
|
||||
---
|
||||
|
||||
### 6. 团期状态流水 `GET /v3/admin/order/group-batch/{groupBatchId}/status-logs`
|
||||
|
||||
**VO**: `List<GroupBatchStatusLogItemVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「操作记录 / 时间线」。本次只改 5 个事件的中文标签(PR #8303)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| [].eventType | String | 事件类型值(**不变**) |
|
||||
| [].eventTypeName | String | **取值改变**:见下表;标签在读取时渲染,存量行一并显示新标签 |
|
||||
| [].content | String | 展示文本;本次之后新写入的核单 / 结算相关流水改用新用语,存量行原样 |
|
||||
|
||||
| eventType | 改前 eventTypeName | 改后 eventTypeName |
|
||||
|---|---|---|
|
||||
| `BATCH_TRIP_END` | 返团核团 | 发起核单 |
|
||||
| `BATCH_SETTLE` | 验团结算 | 结算 |
|
||||
| `BATCH_AUDIT_ALLOCATE` | 核团提交核算 | 核单提交核算 |
|
||||
| `BATCH_AUDIT_REALLOCATE` | 核团重新核算 | 核单重新核算 |
|
||||
| `BATCH_AUDIT_PRICE_OVERRIDE` | 核团改价 | 核单改价 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985/status-logs HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"eventType": "BATCH_SETTLE",
|
||||
"eventTypeName": "结算",
|
||||
"fromStatus": "REVIEWING",
|
||||
"fromStatusName": "核单中",
|
||||
"toStatus": "SETTLED",
|
||||
"toStatusName": "已结算",
|
||||
"content": "结算归档,团期结束",
|
||||
"operatorType": "ADMIN"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无流水返回空数组;库里是枚举外历史值时 `eventTypeName` 为 null(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": [], "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 按 `eventType` 做分支的前端不受影响;按中文标签匹配的需改。
|
||||
- `fromStatusName` / `toStatusName`(九态中文名「核单中」「已结算」)不变。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误用法对照
|
||||
|
||||
| 场景 | 用法 |
|
||||
|------|------|
|
||||
| ✅ 看板页签筛选(新) | `opsStage=CONFIGURE` / `opsStage=TRIP` |
|
||||
| ✅ 看板页签筛选(过渡期旧值) | `opsStage=FORMED`,结果 = 配置 + 确认,与改前一致 |
|
||||
| ✅ 页签计数 | 读 `buckets.CONFIGURE`、`buckets.TRIP` 等七桶键;总数读 `total` |
|
||||
| ❌ 总数自己求和 | `Object.values(buckets).reduce(...)` → 七桶与别名重复,结果偏大 |
|
||||
| ❌ 用节点判按钮 | `if (stage === 'CONFIGURE') showConfirmButton()` → 应读 `batchStatus` 与就绪位 |
|
||||
| ❌ 按旧 `opsStage` 值分支 | `case 'FORMED':` → 响应已不再输出旧值 |
|
||||
|
||||
### 核单 / 结算用语:错误码文案变化(PR #8303,码值不变)
|
||||
|
||||
| code | 改前 message | 改后 message |
|
||||
|---|---|---|
|
||||
| 589555 | 该团期已验团归档,不可重复验团 | 该团期已结算归档,不可重复结算 |
|
||||
| 589565 | 团期已验团归档,如需重新核单请先做验团反确认 | 团期已结算归档,如需重新核单请先做结算反确认 |
|
||||
| 589567 | 该团期尚未进入核团:{0} | 该团期尚未进入整团核单:{0} |
|
||||
| 589568 | 核团当前状态不允许该操作:{0} | 整团核单当前状态不允许该操作:{0} |
|
||||
| 589572 | 所选订单不属于本团期的核团范围 | 所选订单不属于本团期的整团核单范围 |
|
||||
| 589573 | 核团数据已被他人修改(当前版本 {0},提交版本 {1}),请刷新后重试 | 整团核单数据已被他人修改(当前版本 {0},提交版本 {1}),请刷新后重试 |
|
||||
|
||||
核单状态枚举 `CHECKED` 的中文名由「已验团」改为「已结算」(存储值不变);前端按 code 判断即可,按中文匹配的需改。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- `batchStatus` 为 null 或不在九态内的脏数据行:四个新字段与 `opsStage` / `opsStageName` 同为 null,不抛错。
|
||||
- `opsStage` 空白或非法值:忽略该筛选并记 warn,不报错。
|
||||
- 未建团行恒为 `RECRUIT` / 招募。
|
||||
- 旧八桶别名(筛选入参 + 统计条 6 键)在前端切到七桶之前保留,删除时另发契约变更。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### stage / opsStage(GroupBatchStageBuckets.Bucket)
|
||||
|
||||
**所属字段**: `stage`、`opsStage`(响应)、`opsStage`(筛选入参)、`buckets` 前 7 键 | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `RECRUIT` | 招募 | ← `RECRUITING` |
|
||||
| `CONFIGURE` | 配置 | ← `RESOURCE_PREPARING` |
|
||||
| `CONFIRM` | 确认 | ← `MATERIAL_PREPARING` |
|
||||
| `TRIP` | 出行 | ← `PENDING_DEPARTURE` / `TRAVELLING` / `TRIP_FINISHED` |
|
||||
| `REVIEW` | 核单 | ← `REVIEWING` |
|
||||
| `SETTLE` | 结算 | ← `SETTLED` |
|
||||
| `DISBANDED` | 已流团 | ← `CANCELLED` |
|
||||
|
||||
### tripSubStatus(GroupBatchStageBuckets.TripSubStatus)
|
||||
|
||||
**所属字段**: `tripSubStatus` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `PENDING_DEPARTURE` | 待出发 | 仍可发起流团 |
|
||||
| `TRAVELLING` | 出行中 | 出发日起 |
|
||||
| `TRIP_FINISHED` | 已返团 | 返团日次日起;首次录共享成本即进入核单 |
|
||||
|
||||
### 旧八桶别名(GroupBatchStageBuckets.LegacyBucket,过渡期)
|
||||
|
||||
**所属字段**: `opsStage`(筛选入参)、`buckets` 后 6 键 | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `FORMED` | 已成团 | = `RESOURCE_PREPARING` + `MATERIAL_PREPARING` |
|
||||
| `PENDING_TRIP` | 待出行 | = `PENDING_DEPARTURE` |
|
||||
| `TRAVELLING` | 出行中 | = `TRAVELLING` |
|
||||
| `TRIP_FINISHED` | 出行完毕 | = `TRIP_FINISHED` |
|
||||
| `AUDITING` | 核团中 | = `REVIEWING` |
|
||||
| `CHECKED` | 已验团 | = `SETTLED` |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `stage` / `stageName` | 无 | 新增,七个节点 |
|
||||
| `tripSubStatus` / `tripSubStatusName` | 无 | 新增,仅出行节点有值 |
|
||||
| `opsStage`(响应) | 八桶:`RECRUIT` / `FORMED` / `PENDING_TRIP` / `TRAVELLING` / `TRIP_FINISHED` / `AUDITING` / `CHECKED` / `DISBANDED` | 七桶,与 `stage` 同值 |
|
||||
| `opsStageName`(响应) | 招募中 / 已成团 / 待出行 / 出行中 / 出行完毕 / 核团中 / 已验团 / 已流团 | 招募 / 配置 / 确认 / 出行 / 核单 / 结算 / 已流团 |
|
||||
| `buckets`(统计条) | 8 键,`total` = 8 桶之和 | 13 键(七桶 + 6 别名),`total` = 七桶之和 |
|
||||
| 导出「状态」列 | 旧八桶中文名 | 节点名,出行节点拼子状态 |
|
||||
| `eventTypeName`(5 个核单 / 结算事件) | 核团 / 验团用语 | 核单 / 结算用语 |
|
||||
| 6 个错误码 message | 核团 / 验团用语 | 核单 / 结算用语(见「四」) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `opsStage=FORMED` 筛选 | 配置 + 确认 | 不变(别名按原口径) |
|
||||
| `opsStage=TRIP` 筛选 | 非旧八桶 code,按非法值忽略(不筛) | 筛待出发 + 出行中 + 已返团 |
|
||||
| `opsStage=CONFIGURE` / `CONFIRM` / `REVIEW` / `SETTLE` | 非法值被忽略 | 按新节点筛选 |
|
||||
|
||||
⚠️ 注意 `TRIP` 与旧 `TRIP_FINISHED` 的区别:旧八桶里「出行完毕」的 code 是 `TRIP_FINISHED`,不是 `TRIP`;页签若传的是 `TRIP_FINISHED`,行为不变。
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 部分。筛选入参与统计条旧键完全兼容;响应字段 `opsStage` / `opsStageName` 的取值改变,读这两个字段做分支或展示的代码会受影响。
|
||||
- **前端是否必须同步上线**: 否。旧页签传旧 code、读旧键仍然工作;要展示六节点需改读 `stage` / `tripSubStatus` 与七桶键。
|
||||
- **前端 workaround 清理点**: 若前端曾自己把九态折叠成阶段,可改为直接读 `stage`;切到七桶后告知后端删除旧八桶别名。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 上述 6 个查询 / 导出接口的节点相关字段与文案,及 6 个核单 / 结算错误码的 message。
|
||||
- **零影响**:
|
||||
- 持久九态 `batchStatus` 及其中文名 `batchStatusName`(「核单中」「已结算」原本就是这两个字)
|
||||
- 所有写接口的状态流转与业务闸(节点只供展示)
|
||||
- 错误码 code 值、枚举存储值、时间线 `eventType` 值
|
||||
- 权限码、路径、信封结构、网关配置
|
||||
- 小程序端
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署 `dev-v3` @ `b212cb708`,2026-09-23 17:44–17:48,`api.test.1814.love:9443`。
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|---|---|
|
||||
| 分页(`pageSize=100`)与看板行(187 行)字段 | 均带 `stage` / `stageName` / `tripSubStatus` / `tripSubStatusName` 四个新字段,且 `opsStage == stage` ✓ |
|
||||
| 九态 → 节点映射 | 全映射实测 ✓ |
|
||||
| 统计条 `summary`(`scope=ALL`) | 13 键逐键与 DB 一致;`total = 267` = 七桶之和(13 键合计 492) ✓ |
|
||||
| `opsStage` 筛选 15 个取值 | `total` 全部等于 DB 计数;`PENDING_TRIP` 只筛待出发 3 条,`FORMED = 215`,未知值忽略 ✓ |
|
||||
| 低权限角色 | `ROOM_MANAGER` / `VEHICLE_MANAGER` → 589507 ✓ |
|
||||
|
||||
核单 / 结算用语(PR #8303:6 个错误码文案、时间线标签):
|
||||
|
||||
补充(PR #8303,dev-v3 @ ade8ac292,2026-09-24 09:52–09:58):api-docs 中「已验团 / 团期核团 / 验团归档 / 验团反确认 / 核团面板」旧文案 0 次;589555「该团期已结算归档,不可重复结算」、589565「团期已结算归档,如需重新核单请先做结算反确认」实测触发;`status-logs` 对 09-18 旧记录返回新标签「发起核单 / 核单改价 / 核单提交核算 / 核单重新核算 / 结算」(`content` 为写入时原文,不随之变化)。工单 #8271 已验收关单。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #6917 / #6926 | #6904 | 统计条与导出、`opsStage` 筛选首版 | ⚠️ 桶取值被本次替换,旧 code 以别名保留 |
|
||||
| — | #7190 | 加「出行完毕」`TRIP_FINISHED` 成八桶 | ⚠️ 同上 |
|
||||
| — | #7535 | 详情 / 分页透出 `opsStage` | ⚠️ 字段保留,取值改七桶 |
|
||||
| **本 PR #8276 / #8279 / #8303** | **#8271** | 六节点派生 + 七桶 + 核单 / 结算用语 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8271](https://git.1814.love/wx/HL/issues/8271)
|
||||
- 关联 PR: [wx/HL#8276](https://git.1814.love/wx/HL/pulls/8276)、[wx/HL#8279](https://git.1814.love/wx/HL/pulls/8279)、[wx/HL#8303](https://git.1814.love/wx/HL/pulls/8303)
|
||||
- 同批六节点条目:团期人工确认(#8268)、确认后锁定配置(#8269)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8271](https://git.1814.love/wx/HL/issues/8271)
|
||||
- **PR**: [#8276](https://git.1814.love/wx/HL/pulls/8276)、[#8279](https://git.1814.love/wx/HL/pulls/8279)、[#8303](https://git.1814.love/wx/HL/pulls/8303)
|
||||
- **Merge commit**: [678d47b58](https://git.1814.love/wx/HL/commit/678d47b584ac0237784c0bf728e86dfe6cbacb1d)、[bfbc0d337](https://git.1814.love/wx/HL/commit/bfbc0d337a92f3e2dec8b84aa54cbf1042f08698)、[ade8ac292](https://git.1814.love/wx/HL/commit/ade8ac292388354649c3e367e1171e6e55836280)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,438 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8292"
|
||||
title: "订单标签四个写端点补订单归属守卫,与读面口径对齐"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "接口路径、请求体、成功响应结构全未变,变的是失败分支:新增 581064 无权修改该订单。无新增路由,故 gateway_status=not_required。测试服实测(网关 https://api.test.1814.love,部署 ab630a3f8):test_admin(CUSTOMIZER,非归属) POST 他人订单标签 → 581064 且复查无残留;test_admin 对自己订单读/原样全量回写 → 200;wx(SUPER_ADMIN) 对他人订单读/原样回写 → 200;admin(当前角色 GROUP_BATCH_MANAGER)写他人订单 → 581008(#8154 的 Controller 角色守卫先拒)。边缘变化:deleteTag/patchTag 传不存在的 orderId 时错误码由 581410/581411 变为 581401。frontend_status=pending:hl-ui 现按 err.message 透传,581064 走通用 toast 即可,无需改码。 前端 2026-09-24 核验 not_required:四写端点前端只消费 PUT /tags(replaceOrderTags 全量替换,POST/DELETE/PATCH 增删改无封装调用),TagPickerModal catch 纯透 err.message 无按码分支;581064/581410/581411/581401/581433 前端零引用,新增 581064 走通用 toast 透后端原文即达标;581008/581045 是 orderAccess 路由隔离角色守卫与标签业务无关;581410/581411 变形只影响前端无调用的 DELETE/PATCH。零改动。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单标签: 四个写端点补订单归属守卫,与读面口径对齐
|
||||
|
||||
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(端口 8086)
|
||||
> **PR**: [#8307](https://git.1814.love:8443/wx/HL/pulls/8307)
|
||||
> **Issue**: [#8292](https://git.1814.love:8443/wx/HL/issues/8292)
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 订单详情页「标签」区四个写操作的**权限口径**;接口路径、请求体、成功响应结构均未变
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(本版与上版行为不同,必读)
|
||||
|
||||
**这四个写接口现在会拒绝「非本单归属人」的调用,失败时返回新错误码 `581064`(无权修改该订单)。**
|
||||
|
||||
- 本次变了什么:四个写端点补上了**订单归属**校验。
|
||||
- 前端以前以为的:除团期管理员外谁都能写(确实是 #8290 之前的行为)。
|
||||
- 实际现在是什么:与**读面**(#8290 已收紧的 `GET .../tags`、`GET .../tag-picker`)**同一套放行集合**——读得到的单才写得动。
|
||||
|
||||
前端以前只会从这四个接口收到 `581008`(团期管理员)与 `581045`(房务),**现在会新增收到 581064**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
#8170 AC-18(PR #8290)给 `GET /v3/admin/order/{orderId}/tags`、`GET .../tag-picker` 与 `GET .../itinerary-document` 补上了 `OrderViewGuard.assertOrderReadable`,读面收紧后同一批数据上的四个**写**端点仍零归属校验,形成「**不能看,但能改**」——任意后台角色拿到 `orderId` 就能增、能改、能删他人订单的标签,其中 `PUT .../tags` 是全量替换,会清掉不在入参里的标签。
|
||||
|
||||
口径由 wx 于 2026-09-24 定案:**写面与读面读写对称**。本单是 #8170 挂账的「存量治理」单。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 增订单标签 | POST | `/v3/admin/order/{orderId}/tag` | 失败分支新增 | 非归属人 → 581064 |
|
||||
| 2 | 删订单标签 | DELETE | `/v3/admin/order/{orderId}/tag/{tagId}` | 失败分支新增 | 非归属人 → 581064;orderId 不存在时由 581410/581411 改为 581401 |
|
||||
| 3 | 改订单标签 | PATCH | `/v3/admin/order/{orderId}/tag/{tagId}` | 失败分支新增 | 非归属人 → 581064;533 原有「非标签创建人 → 581433」不变 |
|
||||
| 4 | 全量替换订单标签 | PUT | `/v3/admin/order/{orderId}/tags` | 失败分支新增 | 非归属人 → 581064 |
|
||||
|
||||
**四个接口的请求体、成功响应体、路径参数全部未变。**
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 增订单标签 `POST /v3/admin/order/{orderId}/tag`
|
||||
|
||||
**VO**: `TagAddReqVO → Result<OrderTagVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页标签区点击「打标签」后提交。creator / createdAt 由后端从 JWT 派生,前端不传。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderId` | Path | String | ✅ | - | 订单 ID |
|
||||
| `tagName` | Body | String | ✅ | trim 后非空,同订单内不重名 | 标签名 |
|
||||
| `tagColor` | Body | String | ❌ | `#` + 6 位十六进制 | 不传默认 `#5B8FF9` |
|
||||
|
||||
#### 出参 `Result<OrderTagVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `tagId` | String | 雪花 ID,字符串返回防精度丢失 |
|
||||
| `orderId` | String | 订单 ID |
|
||||
| `tagName` | String | 标签名 |
|
||||
| `tagColor` | String | 色值 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "tagName": "重点客户", "tagColor": "#FF6B6B" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "tagId": "2100...", "orderId": "9199...", "tagName": "重点客户", "tagColor": "#FF6B6B" }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态;
|
||||
无下游 Feign/消息依赖,故也没有降级分支。前端按 `code == 200` 判成功、否则 toast `message` 即可。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 581064, "message": "无权修改该订单", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **鉴权**:必须是本单定制师,或 ADMIN / SUPER_ADMIN / 车务管理员;团期管理员被 581008 拒(只读承诺 #8154);房务管理员与房务组长 581045。
|
||||
- **空值**:`tagName` trim 后为空 → 581402;颜色格式非法 → 581404。
|
||||
- **幂等/并发**:同订单同名标签 → 581403(不做幂等 upsert)。
|
||||
- **失败零写入**:归属守卫排在插入之前,被拒时一行都不会写。
|
||||
|
||||
### 2. 删订单标签 `DELETE /v3/admin/order/{orderId}/tag/{tagId}`
|
||||
|
||||
**VO**: `无请求体 → Result<Boolean>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页标签区删除某个已挂标签(软删)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderId` | Path | String | ✅ | - | 订单 ID |
|
||||
| `tagId` | Path | String | ✅ | - | `order_tag.tag_id` |
|
||||
|
||||
#### 出参 `Result<Boolean>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Boolean | true = 删除成功 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/9199000000000000002/tag/2100123456789012345
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": true, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态;
|
||||
无下游 Feign/消息依赖,故也没有降级分支。前端按 `code == 200` 判成功、否则 toast `message` 即可。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 581401, "message": "订单不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **鉴权**:同接口 1。**标签级没有创建人限制**(任何有权者都可删该单的标签),权限完全由订单归属决定。
|
||||
- **顺序**:先取订单 → 判空(581401)→ 归属守卫 → 才按 `tagId` 取标签。越权请求不再能从「标签不存在 581410」与「标签不属于该订单 581411」的差别推断某个 `tagId` 是否存在。
|
||||
- **失败零写入**:守卫在任何标签表访问之前。
|
||||
|
||||
### 3. 改订单标签 `PATCH /v3/admin/order/{orderId}/tag/{tagId}`
|
||||
|
||||
**VO**: `TagPatchReqVO → Result<Boolean>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页标签区编辑某个已挂标签的名称或颜色(PATCH 语义,只改传入字段)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderId` | Path | String | ✅ | - | 订单 ID |
|
||||
| `tagId` | Path | String | ✅ | - | `order_tag.tag_id` |
|
||||
| `tagName` | Body | String | ❌ | trim 后非空 | 不传则不改 |
|
||||
| `tagColor` | Body | String | ❌ | `#` + 6 位十六进制 | 不传则不改 |
|
||||
|
||||
#### 出参 `Result<Boolean>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Boolean | true = 修改成功 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "tagName": "高净值", "tagColor": "#FF6B6B" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": true, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态;
|
||||
无下游 Feign/消息依赖,故也没有降级分支。前端按 `code == 200` 判成功、否则 toast `message` 即可。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 581433, "message": "无权修改该标签(非创建人)", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **两道互不替代的校验**:① 订单归属(581064,本单新增);② **标签级创建人**(操作人 ≠ `order_tag.created_by` → 581433,既有行为未变)。
|
||||
- `581433` 的文案本单由「非创建人**且非主管**」订正为「非创建人」:实现里**从来没有**「主管」判定分支,原文案是空承诺。
|
||||
- **顺序**:先取订单 → 判空(581401)→ 归属守卫 → 才按 `tagId` 取标签。
|
||||
- 操作人上下文缺失时第二道校验整体放行(既有 fail-open),故它**不是**归属防线;归属防线是第一道。
|
||||
|
||||
### 4. 全量替换订单标签 `PUT /v3/admin/order/{orderId}/tags`
|
||||
|
||||
**VO**: `ReplaceOrderTagsReqVO → Result<OrderTagListRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
打标签弹窗点「确定」后整批提交(前端先 `GET .../tag-picker` 再提交全量列表)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `orderId` | Path | String | ✅ | - | 订单 ID |
|
||||
| `tags[].tagName` | Body | String | ✅ | trim 后非空 | 标签名(按名去重,保留首个) |
|
||||
| `tags[].tagColor` | Body | String | ❌ | `#` + 6 位十六进制 | 不传默认 `#5B8FF9` |
|
||||
|
||||
#### 出参 `Result<OrderTagListRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `attached` | Array | 替换后的全量标签快照(元素同 `OrderTagVO`) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "tags": [ { "tagName": "重点客户", "tagColor": "#FF6B6B" } ] }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "attached": [ { "tagId": "2100...", "orderId": "9199...", "tagName": "重点客户", "tagColor": "#FF6B6B" } ] }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态;
|
||||
无下游 Feign/消息依赖,故也没有降级分支。前端按 `code == 200` 判成功、否则 toast `message` 即可。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 581064, "message": "无权修改该订单", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **这是四个写端点里后果最重的一个**:全量替换按 diff 清掉不在入参里的标签。传空 `tags` 数组 = 一次性清空该单全部标签。
|
||||
- **鉴权**:同接口 1;归属守卫排在 diff 之前(防御性,不做无谓写、不依赖事务回滚兜底)。
|
||||
- **事务**:`@Transactional(rollbackFor = Exception.class)`,diff 失败整笔回滚。
|
||||
- **失败零写入**:被拒时 diff 一步都不执行。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | payload |
|
||||
|------|---------|
|
||||
| ✅ 只改名不改色 | `{ "tagName": "高净值" }` |
|
||||
| ✅ 只改色不改名 | `{ "tagColor": "#FF6B6B" }` |
|
||||
| ✅ 全量替换为空标签 | `{ "tags": [] }`(有权者:清空该单标签) |
|
||||
| ❌ 全量替换传 null | `{ "tags": null }` → 参数校验失败 |
|
||||
| ❌ 颜色少 # 或非 6 位 | `{ "tagColor": "FF6B6B" }` → 581404 |
|
||||
| ❌ 非归属人写他人订单 | 任意上述 payload → **581064** |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无字段互斥关系;PATCH 是「只改传入字段」语义,不需要先读回再整对象提交。若要整批调整,用 `PUT .../tags` 传全量列表(空数组即清空)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
表 `order_tag`(继承 `BaseDO`,含 `deleted_at` 逻辑删除列)。
|
||||
|
||||
| 操作 | SQL 效果 |
|
||||
|------|----------|
|
||||
| POST 增标签 | `INSERT INTO order_tag (tag_id, order_id, tag_name, tag_color, creator, created_by, ...)` |
|
||||
| DELETE 删标签 | **软删** `UPDATE order_tag SET deleted_at = NOW() WHERE tag_id = ?`(`BaseDO.deletedAt` 带 `@TableLogic(value = "NULL", delval = "NOW()")`) |
|
||||
| PATCH 改标签 | `UPDATE order_tag SET tag_name = ?, tag_color = ? ...`(仅传入字段) |
|
||||
| PUT 全量替换 | 差集软删 + 新增 INSERT + 颜色变更 UPDATE;命中 `user_tag_library` 的标签会 `touchUsage` 累加 `used_count` 并刷新 `last_used_at` |
|
||||
|
||||
**读面一致性**:所有 `selectByOrderId` 查询被 `@TableLogic` 自动附加 `deleted_at IS NULL`,被软删的标签即刻从接口结果消失,但行仍留在库里(无业务恢复入口)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- `orderId` 不存在 → `POST` / `PUT` 返 581401;`DELETE` / `PATCH` **本单起也返 581401**(此前会走到 581410/581411)
|
||||
- `tagId` 不存在 → 581410;`tagId` 不属于该 `orderId` → 581411(DELETE)/ 581434(PATCH)
|
||||
- 同订单同名标签 → 581403(新增时)
|
||||
- 团期管理员 → 581008(#8154 的角色级只读,Controller 层先拒,与 `groupBatchId` 无关)
|
||||
- 房务管理员 / 房务组长 → 581045
|
||||
- 老数据兼容:存量标签的 `created_by` 若为空,PATCH 的第二道校验仍按操作人比对(既有行为,本单未改)
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
本节不适用:四个接口无枚举入参或返回值(标签类型字段已由 #3941 移除)。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 请求体字段 | — | **无变化** |
|
||||
| 响应体字段 | — | **无变化** |
|
||||
| 失败错误码 | 581008(团期管理员)/ 581045(房务) | 新增 **581064**(非归属人);581008 / 581045 保持 |
|
||||
| DELETE/PATCH 的 `orderId` 不存在 | 581410 / 581411(先查标签) | **581401 订单不存在**(先查订单) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 非归属角色写他人订单标签(POST / DELETE / PATCH / PUT) | **放行** | **581064** |
|
||||
| 非归属角色全量替换他人订单标签(PUT 空数组) | 放行,会清空他人标签 | 581064,零写入 |
|
||||
| 本单定制师写自己订单标签 | 放行 | 放行(不变) |
|
||||
| ADMIN / SUPER_ADMIN / 车务管理员 | 放行 | 放行(不变) |
|
||||
| 团期管理员 | 581008 | 581008(不变) |
|
||||
| 房务管理员 / 房务组长 | 放行 | 581045 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:**对合法调用方否**(有权角色行为零变化);对**越权调用**是有意的行为收紧。
|
||||
- **前端是否必须同步上线**:**否**。接口路径与结构未变,581064 走通用 `err.message` toast 即可;但要**知悉**该码的语义(是写权限,不是登录态问题)。
|
||||
- **前端 workaround 清理点**:无。hl-ui 现按 `err.message` 透传,无按码分支可清理。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明,帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:订单详情页「标签」区的四个写操作(增 / 删 / 改 / 全量替换)。
|
||||
- **零影响**:
|
||||
- 标签的**三个读接口**(`GET .../tags`、`GET .../tag-picker`、`GET .../itinerary-document`)——本单未改
|
||||
- 创单链路写入标签(`OrderCreateTransactionExecutor` 同事务内 batchCreate,无「他人订单」概念)
|
||||
- 订单详情 / 行程 / 财务等其它读写面
|
||||
- `user_tag_library` 私人标签库的全部接口
|
||||
- 存量数据(不迁移;软删语义未变)
|
||||
- 其它服务(本改动不跨服务,无 Feign/事件/MQ 契约变化)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关调用(`https://api.test.1814.love`,测试服部署 `★ab630a3f8`):
|
||||
|
||||
```
|
||||
GET /v3/admin/order/9199000000000000002/tags as CUSTOMIZER(非归属) → 581008 无权查看此订单 ✓
|
||||
POST /v3/admin/order/9199000000000000002/tag as CUSTOMIZER(非归属) → 581064 无权修改该订单 ✓
|
||||
GET /v3/admin/order/9199000000000000002/tags as SUPER_ADMIN → 200 复查确认无残留标签 ✓
|
||||
PUT /v3/admin/order/9199000000000000002/tags as SUPER_ADMIN(原样回写)→ 200 ✓
|
||||
GET /v3/admin/order/2096701868536188930/tags as CUSTOMIZER(本单定制师)→ 200 ✓
|
||||
PUT /v3/admin/order/2096701868536188930/tags as CUSTOMIZER(本单定制师)→ 200 ✓
|
||||
POST /v3/admin/order/9199000000000000002/tag as GROUP_BATCH_MANAGER → 581008(#8154 角色守卫先拒) ✓
|
||||
```
|
||||
|
||||
单元与门禁:定向 `Tests run 129 / Failures 0 / Errors 0`;ArchTest `Tests run 26 / Failures 0 / Errors 0`。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR(纠错 / 功能演进时必写)
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #8290 | #8170 | 标签**读**面补 `assertOrderReadable`,本单就是它显影出的写面缺口 | ✅ 有效 |
|
||||
| **本 PR #8307** | **#8292** | 标签**写**面补归属守卫 + 新写面码 581064 + 581433 文案订正 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8292](https://git.1814.love:8443/wx/HL/issues/8292)
|
||||
- 关联 PR: [wx/HL#8307](https://git.1814.love:8443/wx/HL/pulls/8307)
|
||||
- 后续计划: 无
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8292](https://git.1814.love:8443/wx/HL/issues/8292)
|
||||
- **PR**: [#8307](https://git.1814.love:8443/wx/HL/pulls/8307)
|
||||
- **Merge commit**: [ab630a3f8](https://git.1814.love:8443/wx/HL/commit/ab630a3f8)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,306 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8294"
|
||||
title: "团期配车就绪检查「座位不足」黄牌改为扣司机座,新增 passengerSeatTotal"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "e5a34cd13e641ac10394a9a245f0548461894a02"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "PR #8305 已合并 dev-v3(2a3b9df59)。部署:hl-fleet-service dev-v3 @ 2a3b9df59,2026-09-24 09:48:52 起滚,09:50:13 完成,8087/8187 两实例均 UP;实测 09:52 晚于部署完成时刻。测试服网关真实 GET /admin/fleet/group-dispatch/batches/{groupBatchId}/readiness:团期 2101514348969226242 返回 WARN_SEAT_SHORTAGE,seatTotal=7、passengerSeatTotal=6、headcount=40、gap=34(7-1=6 证明扣座、40-6=34 证明 gap 按新口径);团期 2102066067272826881 座位合计 0 时 passengerSeatTotal=0 不为负;团期 2101997326354862082(座位合计 5/可载客 4/人数 4)与 2101690789438570497(7/6/6)warned=false,证明取等号时不误报。fleet 定向单测 778/0/0(含 GroupDispatchReadinessServiceTest 26、VehicleSeatCapacityTest 4、FleetRedLineArchTest 18)+ spotless:check 绿;回退判据后 3 个用例转红(含「20 座车 20 人应报缺 1 座」),恢复后转绿。gateway_status: verified —— 路径本就在既有 admin-fleet-service 路由 Path=/admin/fleet/** 下,本次零路由改动,并已通过真实网关实测。frontend_status: pending —— 前端需读新字段 passengerSeatTotal,且 gap 的语义已变(改为 用车人数 − 已扣司机座的可载客数),若仍按「用车人数 − seatTotal」渲染会差 1×车数。 前端已交付并验证:实证黄牌 b.message 直显、seatTotal/gap/headcount 零数值消费,新文案自动生效;group-dispatch.js JSDoc 订正新口径(透 message 禁文本匹配/自算 gap,seatTotal+gap≠headcount 正常须 passengerSeatTotal+gap 闭合),drawer spec fixture/断言改新文案回归锁 7 例全绿。hl-admin@e5a34cd1。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet 团期配车就绪检查: 座位不足黄牌改为扣司机座
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service(团期配车读口)
|
||||
> **PR**: #8305
|
||||
> **Issue**: #8294
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「团期详情 → 配车」页的就绪检查提示(黄牌文案与字段)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:`GET /admin/fleet/group-dispatch/batches/{groupBatchId}/readiness` 响应里 `warnings[]` 中 `code=WARN_SEAT_SHORTAGE` 的条目,**判据由「座位合计(含司机座)< 该日用车人数」改为「可载客数合计 < 该日用车人数」**。每车可载乘客数 = 座位数 − 1(扣 1 个司机座)。口径与团级用车需求 809116(#8278,PR #8296)、fleet 单车派车完全一致。
|
||||
- 前端以前以为的:`seatTotal` 就是「能坐多少人」,`gap = headcount − seatTotal`。**现在 `gap` 不再按 `seatTotal` 算**——同一响应里 `seatTotal + gap ≠ headcount` 是正常的,必须叠加新字段 `passengerSeatTotal` 才闭合(`passengerSeatTotal + gap = headcount`)。
|
||||
- 实际现在的行为:判据字段是 **`passengerSeatTotal`**;`seatTotal` **原义与数值都未变**(仍是含司机座的座位合计,可以直接继续展示「N 座车」)。
|
||||
- **会新增黄牌**:`座位合计 ≥ 用车人数 > 可载客数合计` 这个区间以前不报、现在报。最小例子:1 辆 20 座车、该日 20 人——改前不报,改后报「缺 1 座」(司机没座)。存量数据不重算,下一次读取即按新口径。
|
||||
- `message` 文案改写(**前端如果做文本匹配会失效**):旧「`2027-06-10 分组 MAIN 座位合计 7,该日用车人数 40,缺 33 座`」→ 新「`2027-06-10 分组 MAIN 座位合计 7 座(含司机座),已扣司机座后可载客 6 人,该日用车人数 40,缺 34 座`」。
|
||||
- **黄牌语义不变**:仍然只提醒、不阻断,`ready` 与 `warned` 仍互相独立,`ready=true && warned=true` 依然合法。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
#8278 已定案:团级用车需求的容量一律按「每车扣 1 个司机座」算(`VehicleSeatCalculator`),订单子级与 fleet 单车派车(预检告警、候选容量)本来就是这个口径。唯一没对齐的是 fleet 的**团级**就绪检查——它直接累加 `vehicle.getSeats()`,把「20 座车塞 20 人」判成够。同一份排法在团级需求侧已被 809116 拦下,在配车就绪页却显示「够」,运营无法判断该信哪一个。本单让第三处(也是最后一处)向已有口径看齐,而不是放松另外两处。
|
||||
|
||||
最强反例(评审已确认,留档):「刚好坐满、司机另开一辆车」这类排法在新口径下会多出一条黄牌。它只是提醒不是硬拦,且这类配置在业务上本就不成立(司机座不能卖给乘客);被提醒是纠正而不是误伤。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期配车就绪检查 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/readiness` | 响应新增字段 + 字段语义变更 + 文案变更 | `warnings[]` 中 `WARN_SEAT_SHORTAGE` 条目新增 `passengerSeatTotal`,`gap` 改按它计算,`message` 改写 |
|
||||
|
||||
**本次只动这一个端点**(同一控制器的其余端点未改)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期配车就绪检查 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/readiness`
|
||||
|
||||
**VO**: `GroupDispatchReadinessRespVO → GroupDispatchReadinessItemVO[]`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「配车」页进入时拉取,用于展示硬拦(不能置 vehicle_ready)与黄牌(能发车但有缺口)。请求参数与响应整体结构均不变,本条只改「只提醒」数组里座位不足那一档的字段与文案。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `groupBatchId` | Path | Long | 是 | 团期主订单 ID | 路径参数 |
|
||||
|
||||
#### 出参 `Result<GroupDispatchReadinessRespVO>`
|
||||
|
||||
**顶层字段**(本次未变):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `groupBatchId` | String(Long) | 团期主订单 ID |
|
||||
| `requirementId` | String(Long) | 判定所依据的正式需求 ID |
|
||||
| `requirementVersion` | Integer | 判定所依据的需求版本 |
|
||||
| `planVersion` | Long | fleet 侧当前计划版本(该团尚无任何计划行时为 null) |
|
||||
| `ready` | Boolean | 硬拦三项是否全过(`= blockers.isEmpty()`) |
|
||||
| `warned` | Boolean | 是否有只提醒项(`= !warnings.isEmpty()`) |
|
||||
| `blockers` | Array | 硬拦未过项 |
|
||||
| `warnings` | Array | 只提醒项 |
|
||||
| `groups` | Array | 逐组覆盖明细(与配车写口 `coverage` 同源,逐字段可比对) |
|
||||
| `shareGroupCount` | Integer | 本团 active 同团车辆共用关系数(只供展示,不参与判定) |
|
||||
|
||||
**`warnings[]` 元素(`GroupDispatchReadinessItemVO`)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | String | 判定项代码:`WARN_SEAT_SHORTAGE` / `WARN_DRIVER_MISSING` |
|
||||
| `message` | String | 中文描述(本次改写,见下表) |
|
||||
| `tripDate` | LocalDate | 相关行程日 |
|
||||
| `groupCode` | String | 相关乘车分组码 |
|
||||
| `seatTotal` | Integer | 该日该组的座位合计(含司机座);仅座位不足档有值 |
|
||||
| `passengerSeatTotal` | Integer | **新增**:该日该组的可载客数合计(Σ max(0, seats − 1));仅座位不足档有值 |
|
||||
| `headcount` | Integer | 该日该组的用车人数;仅座位不足档有值 |
|
||||
| `gap` | Integer | 座位缺口(本次改按 `passengerSeatTotal` 计算);仅座位不足档有值 |
|
||||
| `dispatchId` | String(Long) | 相关配车行 ID;仅司机缺失档有值 |
|
||||
|
||||
**本次逐字段变化(仅 `WARN_SEAT_SHORTAGE` 档)**:
|
||||
|
||||
| 字段 | 类型 | 本次变化 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `code` | String | 不变 | `WARN_SEAT_SHORTAGE` / `WARN_DRIVER_MISSING` |
|
||||
| `message` | String | **改写** | 座位不足档现在写明「含司机座」与「已扣司机座后可载客 N 人」 |
|
||||
| `tripDate` | LocalDate | 不变 | 相关行程日 |
|
||||
| `groupCode` | String | 不变 | 相关乘车分组码 |
|
||||
| `seatTotal` | Integer | **语义不变** | 该日该组活跃配车行的**座位合计(含司机座)**,数值与改前逐字相同 |
|
||||
| `passengerSeatTotal` | Integer | **新增** | 该日该组的**可载客数合计** = Σ `max(0, seats − 1)`,座位数取不到的车按 0 计,恒 ≥ 0 |
|
||||
| `headcount` | Integer | 不变 | 该日该组用车人数 |
|
||||
| `gap` | Integer | **语义变更** | 由 `headcount − seatTotal` 改为 `headcount − passengerSeatTotal`(恒 ≥ 1) |
|
||||
| `dispatchId` | String(Long) | 不变 | 仅 `WARN_DRIVER_MISSING` 有值 |
|
||||
|
||||
以上与座位相关的五项**仅 `WARN_SEAT_SHORTAGE` 档有值**;`WARN_DRIVER_MISSING` 档这五项均为 `null`(含新增的 `passengerSeatTotal`)。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/group-dispatch/batches/2101514348969226242/readiness
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服真实响应(2026-09-24 09:52,`hl-fleet-service` dev-v3 @ `2a3b9df59`,节选该接口 `warnings` 内容):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"groupBatchId": "2101514348969226242",
|
||||
"ready": true,
|
||||
"warned": true,
|
||||
"planVersion": 4,
|
||||
"warnings": [
|
||||
{
|
||||
"code": "WARN_SEAT_SHORTAGE",
|
||||
"message": "2027-06-10 分组 MAIN 座位合计 7 座(含司机座),已扣司机座后可载客 6 人,该日用车人数 40,缺 34 座",
|
||||
"tripDate": "2027-06-10",
|
||||
"groupCode": "MAIN",
|
||||
"seatTotal": 7,
|
||||
"passengerSeatTotal": 6,
|
||||
"headcount": 40,
|
||||
"gap": 34,
|
||||
"dispatchId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对照:`seatTotal(7) + gap(34) = 41 ≠ headcount(40)`;`passengerSeatTotal(6) + gap(34) = 40 = headcount` —— **前端做守恒校验请用后者**。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 无缺口时 `warnings: []`、`warned: false`,其余字段照常返回。
|
||||
- 该团未声明任何乘车分组或基线不可用:`code=602113`,**失败关闭**,绝不返回 `ready=true`。
|
||||
- 入参非法:`code=602114`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 602113, "message": "...", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 座位不足**只提醒**:`ready` 只看 `blockers`,不受本档影响;两条写路径(配车方向判定、就绪意图发射)只调 `hardGatesPass`,该入口完全不看座位。
|
||||
- 扣座是**逐车**的:N 辆车扣 N 个司机座(两辆 20 座车 / 39 人 ⇒ 可载客 38、缺 1 座),不是全团只扣 1 个。
|
||||
- 座位数取不到(车已软删 / 车型未录座位数)的车按 0 计,可载客数**不出现负数**。
|
||||
- 按「组 + 行程日」逐格比:同一组不同日期分别判定,不同组分别判定。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
### ✅ 正确 / ❌ 错误用法对照
|
||||
|
||||
| 场景 | ✅ 正确 | ❌ 错误 |
|
||||
|---|---|---|
|
||||
| 判断「够不够」 | 后端已给结论:`warned` / `warnings` 是否为空 | 前端自己用 `seatTotal >= headcount` 重算 |
|
||||
| 展示缺口 | 直接展示 `gap`,或展示 `headcount − passengerSeatTotal` | 用 `headcount − seatTotal` 现算(会少 1×车数) |
|
||||
| 展示运力 | `passengerSeatTotal`(可载客)与 `seatTotal`(车辆座位规格)分开显示 | 把 `seatTotal` 当成可载客数展示 |
|
||||
| 文案 | 直接展示后端 `message` | 按旧文案做字符串匹配/替换 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无。本接口是只读 GET,不改变任何状态,也不触发重算。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
无写入。判据全部基于既有列在读取时现算(活跃配车行 × 车辆座位数 × 需求逐日人数),**不落库、不新增列、无迁移**。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 边界 | 行为 |
|
||||
|---|---|
|
||||
| `headcount = 0` | 不报(`0 >= 0`) |
|
||||
| 座位数取不到(车软删 / 未录) | 该车按 0 计,缺口如实报出 |
|
||||
| 座位数为 0 或 1 | 可载客数 0,不出现负数 |
|
||||
| 恰好坐满(可载客数 == 人数) | 不报(取等号判「够」) |
|
||||
| 该日没有排车 | 该日可载客数 0;整组没排车由硬拦 `BLOCK_GROUP_MISSING` 承接 |
|
||||
| 需求已 DONE | 仍返回 `ready=true`(DONE 比 CONFIRMED/DISPATCHED 更靠后) |
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
`warnings[].code` 取值不变,仍为两档:
|
||||
|
||||
| 取值 | 含义 | 相关字段 |
|
||||
|---|---|---|
|
||||
| `WARN_SEAT_SHORTAGE` | 该日该组可载客数不足 | `tripDate` / `groupCode` / `seatTotal` / `passengerSeatTotal` / `headcount` / `gap` |
|
||||
| `WARN_DRIVER_MISSING` | 配车行未排司机(消息带车牌) | `tripDate` / `groupCode` / `dispatchId`,其余为 `null` |
|
||||
|
||||
**无新增错误码。**
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比(`warnings[]` 中 `WARN_SEAT_SHORTAGE` 条目)
|
||||
|
||||
| 字段 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| `seatTotal` | 座位合计(含司机座) | **不变**(同值同义) |
|
||||
| `passengerSeatTotal` | 不存在 | **新增**,`Σ max(0, seats − 1)` |
|
||||
| `gap` | `headcount − seatTotal` | `headcount − passengerSeatTotal` |
|
||||
| `message` | `{日期} 分组 {组} 座位合计 {N},该日用车人数 {M},缺 {K} 座` | `{日期} 分组 {组} 座位合计 {N} 座(含司机座),已扣司机座后可载客 {P} 人,该日用车人数 {M},缺 {G} 座` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 修改前 | 修改后 |
|
||||
|---|---|---|
|
||||
| 1 辆 20 座车 / 该日 20 人 | 不报(20 ≥ 20) | **报,缺 1 座** |
|
||||
| 1 辆 20 座车 / 该日 19 人 | 不报 | 不报(可载客 19 ≥ 19) |
|
||||
| 2 辆 20 座车 / 该日 39 人 | 不报(40 ≥ 39) | **报,缺 1 座**(可载客 38) |
|
||||
| 1 辆 7 座车 / 该日 40 人 | 报,`seatTotal=7`、`gap=33` | 报,`seatTotal=7`、`passengerSeatTotal=6`、`gap=34` |
|
||||
| 座位数为 0 / 取不到 | 报,`seatTotal=0`、`gap=人数` | 报,`passengerSeatTotal=0`、`gap=人数`(不为负) |
|
||||
| `ready` | 不受本档影响 | **不变**(仍只提醒) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **变宽**:`座位合计 ≥ 用车人数 > 可载客数合计` 这个区间由「不报」变「报」。区间宽度恰是「该日排的车数」(每车多算 1 个司机座),所以车越多、越容易落进来。
|
||||
- **不变**:`gap` 的**值**在「座位取不到」和「原本就严重不足」的场景里可能不变;但在「刚好卡边界」的场景会 +1×车数。任何拿 `gap` 做阈值判断的前端逻辑都要复核。
|
||||
- **后端消费方**:`getSeatTotal()` / `getGap()` 在 HL 全仓(Java)生产代码中消费方为 **0**(只有测试引用),本接口的消费方是管理后台前端。
|
||||
- **存量数据不重算**:不落库,下一次读取即按新口径;测试服现有 116 条活跃配车行、34 个团期中,**没有**落在新增黄牌区间(`用车人数 == 座位合计`)的活跃分组,本次口径变更对既有数据的可见影响为 0。
|
||||
- 前置依赖:本单与 #8278 是同一口径的第三处收口,**不改变** 809116、子订单级校验、候选容量的任何行为。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- `blockers[]` 三档与 `ready` 的判定逻辑:**零改动**。
|
||||
- `groups[]`、`shareGroupCount`、`planVersion`、`requirementVersion` 等字段:**零改动**。
|
||||
- 同一控制器的其余端点(`/pending-batches`、`/batches/{id}/overview`、`/resource-schedule`):**零改动**。
|
||||
- 配车写口(提交 / 确认 / 改派 / 删除)与 `coverage`:**零改动**。
|
||||
- 单车派车的预检告警、候选容量、候选页 `passengerCapacity` 字段:**行为零改动**(只把内部重复实现收口到一份工具方法)。
|
||||
- 团级用车需求 809116(#8278 已交付)与 order-v3:**行为零改动**(仅同步了两处已失效的注释)。
|
||||
- 数据库:无迁移、无新列。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
- **定向测试**:`mvn -o -pl hl-fleet-service -am test -Dtest='GroupDispatchReadinessServiceTest*,VehicleSeatCapacityTest*,AssignmentCandidateServiceTest*,AssignmentServiceTest*,AssignmentServiceNoVehicleDeclarationTest*,AssignmentServicePickupDropoffTest*,AssignmentServiceClearCancelledOccupancyTest*,AssignmentServiceResolveExceptionTest*,FleetRedLineArchTest' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false` → **778 / 0 / 0,BUILD SUCCESS**(逐类核对 `Tests run`,报告文件时间均晚于本轮起跑)。
|
||||
- **分辨力**:把判据临时还原为旧口径后 3 个用例转红(含「20 座车 20 人应报缺 1 座」),恢复后 30/0/0 转绿。
|
||||
- **网关验证**:`GET https://api.test.1814.love/admin/fleet/group-dispatch/batches/{groupBatchId}/readiness`(fleet dev-v3 @ `2a3b9df59`,09:52 实测)——团期 `2101514348969226242` 返回 `WARN_SEAT_SHORTAGE`(`seatTotal=7`、`passengerSeatTotal=6`、`headcount=40`、`gap=34`);团期 `2102066067272826881` 座位合计 0 时 `passengerSeatTotal=0`;团期 `2101997326354862082`(5/4/4)与 `2101690789438570497`(7/6/6)`warned=false`。全程只读 GET。
|
||||
- **兼容性结论**:`seatTotal` 语义与数值不变,新增字段为增量、老前端不会因缺字段崩溃;但**任何用 `seatTotal` 反算缺口的旧逻辑会差 1×车数**,必须改读 `passengerSeatTotal`(或直接用 `gap`)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 接口契约:`hl-fleet-service/src/main/java/com/hulalv/fleet/dispatch/controller/GroupDispatchQueryController.java`
|
||||
- 响应结构:`hl-fleet-service/src/main/java/com/hulalv/fleet/dispatch/vo/GroupDispatchReadinessItemVO.java`
|
||||
- 口径工具:`hl-fleet-service/src/main/java/com/hulalv/fleet/common/util/VehicleSeatCapacity.java`
|
||||
- 同口径前置单:#8278 / PR #8296(团级 809116 扣司机座),本单是其 `changelogs-v2/2026-09/23_8278_团级用车分组座位校验扣司机座-修改接口-管理后台.md` 里声明的「fleet 团级就绪检查仍不扣座、由 #8294 跟踪」的收口。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8294](https://git.1814.love/wx/HL/issues/8294)
|
||||
- **PR**: [#8305](https://git.1814.love/wx/HL/pulls/8305)
|
||||
- **Merge commit**: [2a3b9df59](https://git.1814.love/wx/HL/commit/2a3b9df591255be691694023812b3878debfb70e)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,293 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8301"
|
||||
title: "车务看板带日期筛选时,单条 requirement_id 为空的派车行不再清空整个日期窗"
|
||||
consumer: "admin"
|
||||
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: "前端 2026-09-24 核验 not_required:接口契约零变化(路径/参数/字段/错误码全不变),纯后端行为修正——带日期筛选时 requirement_id 空脏行由整窗失败关闭返空改为跳过+WARN,同窗其余订单照常返回。grep 实证:看板列表前端只渲染 records,无对「整窗空」的特殊分支/依赖;fleet/board 前端 requirementId 唯一消费点在 useNoVehicleDeclaration(读详情 /orders/{orderId} 查无车声明),与看板列表日期分支无关。后端修正让数据更完整,前端天然受益零改动。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 看板日期筛选分支的脏行失败关闭收窄为跳过单行
|
||||
|
||||
> **存放目录**: `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service
|
||||
> **PR**: #8310
|
||||
> **Issue**: #8301
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「车务看板」订单列表/网格视图(带日期筛选时)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**带日期筛选时,单条脏数据不再清空整个日期窗。** 此前只要日期窗里存在任意一条 `requirement_id` 为空的派车行,`GET /admin/fleet/board/orders`(带 `startDate/endDate` 或 `startDayFrom/startDayTo`)就整页返回 `records=[]`、`total=0`、`code=200`,**不报错**——页面上看到的是「这几天没有订单」而不是任何异常。现在这条行被**跳过并记 WARN**(WARN 里带 `orderId`/`assignmentId`/`assignmentGroupId`),同一日期窗内其余订单**照常返回**,与不带日期筛选时的既有口径一致。
|
||||
|
||||
**接口契约零变化**:路径、参数、字段、类型、必填性、枚举、错误码全部不变,只修正日期分支的行为。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
车务看板带日期筛选时走一条独立的「当前主单日期候选」扫描分支。该分支原本把四类候选行异常合并成一个失败关闭条件,其中一类是「派车行的 `requirement_id` 为空」——这是历史数据迁移、手工修数或未来写路径缺陷可能留下的占位行。这类行本身匹配不上任何用车需求,在不带日期的路径里只是被跳过并记一条汇总 WARN;但在日期分支里,它会让**同页全部订单**的候选一起作废,表现为整窗静默返回空。
|
||||
|
||||
本次把该条件按成因拆开:真正影响槽位聚合正确性的三类结构性畸形(行对象缺失、主单 ID 缺失、派车组 ID 与派车 ID 同时缺失)以及重复槽位键**仍然失败关闭**,只是各自补一条带定位信息的 WARN;`requirement_id` 为空的行改为逐行剔除并记 WARN。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 看板订单列表 | GET | `/admin/fleet/board/orders` | 行为变更(契约不变) | 带日期筛选时,`requirement_id` 为空的派车行由「整窗失败关闭」改为「跳过该行 + WARN」 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 看板订单列表 `GET /admin/fleet/board/orders`
|
||||
|
||||
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`(`records: BoardOrderRecordVO[]`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「车务看板」订单列表/网格视图(`variant=list|grid`)。车务人员按状态、日期、团号、车型等条件筛出待派/已派订单卡片,逐卡执行改派、派车、查看行程等操作。本次改动只影响**带日期筛选**时该列表返回的记录集合。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| startDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗起(`startDayFrom` 的兼容别名) |
|
||||
| endDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗止(`startDayTo` 的兼容别名) |
|
||||
| startDayFrom | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗起;为空时回落到 `startDate` |
|
||||
| startDayTo | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗止;为空时回落到 `endDate` |
|
||||
| statuses / status | Query | String[] / String | ❌ | 枚举:unassigned/unassigned_urgent/holding/holding_urgent/assigned/canceled/completed | 状态筛选,可多选;`status` 为单值兼容别名 |
|
||||
| vehicleTypeKeys / typeKeys | Query | String[] | ❌ | 车型大类字典值 | 车型多选筛选 |
|
||||
| keyword / driverName / contactName / teamNo / plannerName | Query | String | ❌ | - | 关键字、司机、联系人、团号、定制师等模糊搜索 |
|
||||
| groupBatchId | Query | Long | ❌ | 等值匹配 | 运营团期 ID |
|
||||
| consultantId | Query | Long | ❌ | - | 定制师 ID 精确筛选 |
|
||||
| variant | Query | String | ❌ | `list`/`grid` | 视图口径 |
|
||||
| page(或 pageNo) | Query | Integer | ❌ | ≥1,缺省 1 | 页码 |
|
||||
| pageSize | Query | Integer | ❌ | 1-100 | 每页条数 |
|
||||
|
||||
> 本单不新增、不删除、不改任何参数的类型与必填性;上表只为把「受影响的日期参数」放进完整上下文。
|
||||
|
||||
#### 出参 `Result<BoardOrderPageRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records | BoardOrderRecordVO[] | 本页看板卡片;本次改动只影响带日期筛选时该数组的内容 |
|
||||
| total | Long | 命中总数;带日期筛选时不再被单条脏行清零 |
|
||||
| page | Integer | 当前页码 |
|
||||
| pageSize | Integer | 每页条数 |
|
||||
| records[].id | Long | 派车行/虚拟条目主键 |
|
||||
| records[].orderId / orderNo / teamNo | Long / String / String | 子订单 ID、订单编号、团号 |
|
||||
| records[].assignmentId / assignmentGroupId / requirementId | Long | 派车行 ID、派车分组 ID、当前用车需求 ID |
|
||||
| records[].assignmentStatus | String | 派单状态码 |
|
||||
| records[].virtualPending | Boolean | 虚拟待派卡标记(真实记录恒为 `false`) |
|
||||
| records[].groupVehicleCovered | Boolean | 团车整段接管标记(详见 #8235 交接件) |
|
||||
| records[].urgentBadge / canAssign / dispatchReadOnly | String / Boolean / Boolean | 加急标签、可派车、只读 |
|
||||
| records[].startDate / endDate | LocalDate | 行程起止日期 |
|
||||
| records[].currentVehiclePlate / currentDriverName | String | 当前车辆车牌 / 司机姓名 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/board/orders?startDate=2026-11-01&endDate=2026-11-10&variant=list&pageNo=1&pageSize=20
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": 7330843067599549,
|
||||
"orderId": 2101014943313670001,
|
||||
"orderNo": "HL20261106000001",
|
||||
"teamNo": null,
|
||||
"assignmentId": 7330843067599549,
|
||||
"assignmentGroupId": 7330843067599549,
|
||||
"requirementId": 2101014943313670100,
|
||||
"assignmentStatus": "holding",
|
||||
"virtualPending": false,
|
||||
"groupVehicleCovered": null,
|
||||
"urgentBadge": null,
|
||||
"canAssign": true,
|
||||
"dispatchReadOnly": false,
|
||||
"startDate": "2026-11-06",
|
||||
"endDate": "2026-11-07",
|
||||
"currentVehiclePlate": null,
|
||||
"currentDriverName": null
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无匹配记录时返回空数组与 `total=0`,不报错:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
带日期筛选时的空列表**仍不等同于**「该日期窗确实无单」:日期候选扫描取数失败、结果集缺失、候选行结构畸形(行对象缺失 / 主单 ID 缺失 / 派车组 ID 与派车 ID 同时为空)或槽位键重复时,本页仍按既有 fail-closed 口径整体返回空,但每一种成因现在都会在 `hl-fleet-service` 日志里留下一条可区分的 WARN。不带日期筛选的查询不走这条分支。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未登录或登录已过期",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只影响**带日期筛选**的分支。不带日期的查询、车辆矩阵接口、看板汇总接口的口径与本次改动前完全一致。
|
||||
- `requirement_id` 为空的派车行会被剔除:该行原本在不带日期的路径里也匹配不上任何用车需求、不可能出现在看板上,因此剔除它不会让**本该展示**的卡片消失。
|
||||
- 若某订单在日期窗内**只有**这一条脏行,该订单在带日期的视图里看不到——这与它不带日期时的既有表现一致;让它可见需要的是数据修复,不是看板放行。
|
||||
- 日期候选结果集本身取到空(无订单上下文)时不查派车行,正常返回空页。
|
||||
- 鉴权:管理后台登录态,未登录 401(网关拦截)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误用法对照
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| ✅ 直接使用 `total` 与 `records` | 结构与字段均未变,前端无需做任何解析适配 |
|
||||
| ✅ 把带日期筛选返回的空列表理解为「结构性异常或确实无单」 | 本次改动后,单条 `requirement_id` 为空的脏行不再制造这种空结果;剩余会制造空结果的是结构性畸形(行缺失、主单 ID 缺失、槽位键缺失或重复),均会在服务端留 WARN |
|
||||
| ❌ 为「带日期筛选可能返回空」保留前端兜底或本地缓存回填 | 本次改动后不再是必需;保留也不会出错,但会把真实的结构性畸形空结果一并掩盖 |
|
||||
| ❌ 依赖后端 WARN 的具体文案做前端逻辑 | WARN 只面向排障,文案不是契约 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
无。本接口是只读 GET,跳过脏行的判定完全由后端在读取时完成,不接受前端传参覆盖,也不需要前端在请求前做任何字段准备。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 无匹配记录 → 200 + 空数组、`total=0`,不报错
|
||||
- 日期候选行含 `requirement_id` 为空的行 → 200,该行被跳过并记 WARN,其余订单照常返回(本次改动)
|
||||
- 日期候选行结构畸形(行对象缺失 / 主单 ID 缺失 / 派车组 ID 与派车 ID 同时缺失)或槽位键重复 → 200 + 整页空(既有 fail-closed 口径保留),并新增带定位信息的 WARN
|
||||
- order-v3 日期候选页取数失败、超过单次一致性快照上限或上下文畸形 → 200 + 整页空(既有 fail-closed 口径保留)
|
||||
- 虚拟待派候选层不可用(开关关闭或远端候选不可用)→ 只丢虚拟条目,真实派车行照常返回
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 | 变化说明 |
|
||||
|------|------|------|----------|
|
||||
| 请求 / 响应字段 | 无变化 | 无变化 | 本次为纯行为修正,不新增、不删除、不改类型 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 | 影响 |
|
||||
|------|------|------|------|
|
||||
| 日期窗内某订单存在一条 `requirement_id` 为空的派车行 | **整窗**返回 `records=[]`、`total=0`、`code=200`,服务端无日志 | 只**跳过该行**并记 WARN,同窗其余订单正常返回 | 带日期筛选的列表不再被单条脏数据清空 |
|
||||
| 同一数据**不带日期**查询 | 该行被跳过 + 一条汇总 WARN | 不变 | 无 |
|
||||
| 日期候选出现重复槽位键 | 整页失败关闭,无日志 | 整页失败关闭(不变),新增带重复键样本的 WARN | 排障可定位 |
|
||||
| 日期候选行结构畸形(行缺失 / 主单 ID 缺失 / 槽位键全缺) | 整页失败关闭,无日志 | 整页失败关闭(不变),新增带成因与定位字段的 WARN | 排障可定位 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。请求参数、响应结构、字段类型与错误码全部不变,仅带日期筛选时的记录集合与 `total` 不再被单条脏数据清零。
|
||||
- **前端是否必须同步上线**: 否。前端无需修改即可受益。
|
||||
- **前端 workaround 清理点**: 若前端此前针对「带日期筛选偶发返回空」做过兜底或本地回填,可以在确认后移除;保留不影响功能。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:管理后台「车务看板」订单列表/网格视图(`/admin/fleet/board/orders`)在**带日期筛选**时的记录集合与 `total`。
|
||||
- **零影响**:
|
||||
- 不带日期筛选的看板订单列表口径
|
||||
- 车辆矩阵未派清单、网格与月度统计(`/admin/fleet/matrix/**`,不走日期候选扫描分支)
|
||||
- 看板汇总与统计接口
|
||||
- 派车、改派、确认等写接口
|
||||
- 用车需求与派车行的数据结构、状态机与任何表结构
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
测试服部署(`deploy:test` 单写租约):`hl-fleet-service` 2026-09-24 10:49:54 滚动部署到 `dev-v3 @ cc70c9ba3`(该提交是本次 squash 合并 `48c755fd1` 的后代),实例 8187 / 8087 均 UP 且健康检查通过。
|
||||
|
||||
网关实测(`https://api.test.1814.love`,管理后台登录态切到车务角色):
|
||||
|
||||
| # | 请求 | 修复前 `total` | 修复后 `total` |
|
||||
|---|------|----------------|----------------|
|
||||
| 1 | `GET /admin/fleet/board/orders`(不带日期) | 243 | 243(对照组,未变)✓ |
|
||||
| 2 | `...&startDate=2026-11-01&endDate=2026-11-10`(窗内含脏行) | **0** | **8** ✓ |
|
||||
| 3 | `...&startDayFrom=2026-11-01&startDayTo=2026-11-10`(同窗别名参数) | **0** | **8** ✓ |
|
||||
| 4 | `...&startDate=2026-09-24&endDate=2026-11-04`(窗内不含脏行) | 71 | 71(对照组,未变)✓ |
|
||||
| 5 | `...&startDate=2026-11-06&endDate=2026-11-06`(脏行当天) | **0** | **6** ✓ |
|
||||
| 6 | `...&startDate=2026-11-05&endDate=2026-11-07` | **0** | **6** ✓ |
|
||||
|
||||
服务端逐行 WARN(两个实例各两条,与上表实测同刻,原始行):
|
||||
|
||||
```text
|
||||
2026-09-24 10:50:56.063 [http-nio-8087-exec-1] WARN c.h.f.b.service.BoardCandidateSource - 车务看板列表日期候选剔除无用车需求ID的派单行(本行不参与候选): orderId=2101014943313670146 assignmentId=7330843067599548 assignmentGroupId=7330843067599548
|
||||
2026-09-24 10:50:56.556 [http-nio-8187-exec-2] WARN c.h.f.b.service.BoardCandidateSource - 车务看板列表日期候选剔除无用车需求ID的派单行(本行不参与候选): orderId=2101014943313670146 assignmentId=7330843067599548 assignmentGroupId=7330843067599548
|
||||
```
|
||||
|
||||
那条脏行(`assignment_id=7330843067599548`,`order_id=2101014943313670146`,行程日期 2026-11-06)本身不再出现在任何日期窗结果里——它匹配不上任何用车需求,本就无法在看板渲染;关键变化是它不再让同窗其余订单一起消失。
|
||||
|
||||
本地验证:定向 42/42 绿、看板与矩阵影响面回归 379/379 绿、`spotless:check` 与 `mvn -o -pl hl-fleet-service -am verify` 均 BUILD SUCCESS(该模块 4766 个用例 0 失败 0 错误 6 跳过)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8301](https://git.1814.love/wx/HL/issues/8301)
|
||||
- 关联 PR: [wx/HL#8310](https://git.1814.love/wx/HL/pulls/8310)
|
||||
- 同一接口的上一份交接件(#8235,看板订单记录新增 `groupVehicleCovered`),其中「带日期筛选的既有 fail-closed 口径」一条由本文件取代
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8301](https://git.1814.love/wx/HL/issues/8301)
|
||||
- **PR**: [#8310](https://git.1814.love/wx/HL/pulls/8310)
|
||||
- **Merge commit**: [48c755fd1](https://git.1814.love/wx/HL/commit/48c755fd1feff715f9ec946b55f77207f23dc942)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,518 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8311"
|
||||
title: "团期正式用车需求分组座位数改由车队字典档位下拉选择(可选可改),新增 809124 档位校验与汇总座位兜底诊断"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "debb0bc2e22be878243ea8ffe89dd0b9091656a1"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "PR #8324 squash 合并 dev-v3(1638faf08)。部署:hl-order-service-v3 dev-v3 @ 1638faf08,2026-09-24 12:06:26 滚动部署两实例,deploy-status 读数 BEHIND=0/N STATE=ok;请求路径上 hl-gateway 与 hl-fleet-service 同为 ok、BEHIND 右侧 N。测试服网关真实 PUT 实测(groupBatchId=2102692937584513025,BUS 16座/SUV 5座 DRAFT):BUS 13 座 → code=809124「第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择」;SUV 6 座 → code=809124「…不在车型 suv 的可选档位 [5, 7] 内…」;两次拒绝后回读 version 与 seats 均未变(零写入);原值(BUS 16/SUV 5)→ code=200 且落库一致。aggregate-draft 实测回 seatOptionAdjusted=[](当前测试团期没有非法档位子订单)与 draft BUS=16/SUV=5。定向测试 14 个类全绿(SaveTest 31 / DraftAggregatorTest 24 / AggregateDraftTest 12 / ConfirmCheckTest 19 等)。809123 已被同日并入的 #8219 占用,本单让号到 809124。 | 2026-09-24 mmg 交付:编辑弹窗座位数手输改档位下拉(选项取字典 seatOptions 按 typeKey,存量非法值合成占位回显 rule 前置拦,档位不可得不拦),切车型联动清空座位,既有组/汇总灌入组号只读(手增空白组仍可填),汇总诊断四数组改五数组 seatOptionAdjusted 按三种 reason 分文案带 [groupCode] 定位;809124 无按码分支拦截器透报文(带组号+档位列表)即引导;spec 44 例全绿(3 新例+既有下标适配)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3 团期需求: 分组座位数改为字典档位可选 + 809124 档位校验
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(团期需求域,groupbatch 包)
|
||||
> **PR**: 待建
|
||||
> **Issue**: #8311
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的正式用车需求编辑弹窗(含「自动汇总」)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- **新增错误码 809124**:`PUT .../vehicle-requirement` 现在会校验「该组的 `seats` 必须落在该组 `vehicleType` 在车队车型字典里的可选座位档位(seatOptions)内」,不在档位内整份提交被拒。
|
||||
- **本条与子订单级早已存在的同款校验对齐**:子订单用车需求的 `fleet[].seats` 一直有这条(错误码 **582024**「座位数不在该车型大类可选座位数中,请检查车型库」);团级此前只校验「半填 809118」与「容量不足 809116」,收任意 ≥1 的座位数。本单把团级补齐到同一口径。
|
||||
- **前端交互随之改变**:座位数不再是自由输入框,改为下拉,选项取自 `GET /admin/fleet/vehicle-types/list` 里该 `typeKey` 的 `seatOptions`。**这条校验是硬约束,不是提示**——手输/绕过下拉提交字典外档位会被 809124 拒绝。
|
||||
- **`aggregate-draft` 的 `draft.groups[].seats` 语义变更**:#8220 时它取「组内子订单报的最大单车座位」;现在取**该车型大类档位内的值**(子订单报的值在档位内就取组内最大,不在档位内或没报则兜底到该大类**最大档**),被兜底调整的户逐条列在新字段 `seatOptionAdjusted`。**前端必须展示该字段**,否则「子订单报 13 座、草稿变成 19 座」这件事在页面上不可见。
|
||||
- **组号(`groupCode`)仍由后端生成**(车型大写 + 同车型拆组序号:`BUS`、`BUS2`…),表单不应提供输入框;同车型被拆成多组时,`seatOptionAdjusted[]` 里带 `groupCode` 用于定位是哪一组。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
wx 2026-09-24 定案:正式用车需求编辑弹窗里「单车座位数」应由手输改为**按车队字典档位下拉选择(可选、可改)**。落地时发现团级缺少子订单级早就有的「座位数必须在 seatOptions 内」校验:不补这条,下拉只是页面上的自觉,直接调接口仍能把车队没有的档位(如大巴 13 座)写进正式需求,车务排车时对不上型号。本单同时把「自动汇总」的座位默认值改为落在档位内,并对被调整的户出诊断,避免静默改数。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 新增校验 + 新错误码 | 新增 809124(座位数不在该组车型大类可选档位内);档位不可得复用 809120 |
|
||||
| 2 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 响应新增字段 + 既有字段语义变更 | 新增 `seatOptionAdjusted[]`;`draft.groups[].seats` 改为档位内的值 |
|
||||
| 3 | 车型大类全量列表(复用,不改) | — | — | 复用不改 | 座位下拉数据源(`seatOptions`)仍走 `GET /admin/fleet/vehicle-types/list`,本单未改它;契约见第八节说明 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是**整份全量替换**:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本次只新增一条校验与一个错误码。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
| version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 |
|
||||
| remark | Body | String | ❌ | ≤500 | 整份需求备注 |
|
||||
| groups | Body | Array | ✅ | 可以是空数组 | 全部乘车分组;有在团需车户却零分组抛 809103 |
|
||||
| groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) |
|
||||
| groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group;**表单不应给输入框,用后端生成的只读值** |
|
||||
| groups[].vehicleType | Body | String | ✅ | ≤64,取值见 `/admin/fleet/vehicle-types/list` | 车型大类;不在字典内抛 809119 |
|
||||
| groups[].serviceStartDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本组服务开始日 |
|
||||
| groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 |
|
||||
| groups[].seats | Body | Integer | ❌ | `@Min(1)`;与 count 同填或同空;**本次起必须落在该车型大类的 seatOptions 内** | 单车座位数(含驾驶位);下拉取值见 `GET /admin/fleet/vehicle-types/list` 的 `seatOptions`;存量分组(seats 与 count 皆 null)整条跳过本校验 |
|
||||
| groups[].count | Body | Integer | ❌ | `@Min(1)`;与 seats 同填或同空 | 车辆数量;只填一半抛 809118 |
|
||||
| groups[].specialTags | Body | Array<String> | ❌ | 取值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;字典外编码整份拒绝(809117) |
|
||||
| groups[].remark | Body | String | ❌ | ≤500 | 该组备注 / 其他诉求 |
|
||||
| groups[].days | Body | Array | ✅ | 非空,且正好铺满本组服务日范围 | 逐日用车人数与成员 |
|
||||
| groups[].days[].tripDate | Body | LocalDate | ✅ | 落在本组服务日范围内、不重复、不缺日 | 越界或重复抛 809105,缺日抛 809106 |
|
||||
| groups[].days[].headcount | Body | Integer | ✅ | `@Min(1)`,且 ≥ 当日成员户数 | 该组该日乘车人数 |
|
||||
| groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
|
||||
|
||||
#### 出参 `Result<GroupVehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
|
||||
| groupBatchId | String | 团期聚合主键 |
|
||||
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
|
||||
| version | Integer | 版本号,下次提交须回传 |
|
||||
| remark | String | 整份备注 |
|
||||
| confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null |
|
||||
| groups | Array | 全部乘车分组;整团免车态为空数组 |
|
||||
| groups[].groupCode | String | 分组键 = 车费 alloc_group(后端生成,前端只读回显) |
|
||||
| groups[].vehicleType / vehicleTypeName | String | 车型大类 key / 中文名 |
|
||||
| groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null |
|
||||
| groups[].count | Integer | 车辆数量;存量分组为 null |
|
||||
| groups[].totalSeatCount | Integer | 总座位数 = seats × count;缺一即 null |
|
||||
| groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 |
|
||||
| groups[].remainingPassengerSeats | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;**可为负**,是缺口展示值,**不得据此拦截提交**(见 22_8152) |
|
||||
| groups[].days[] | Array | 逐日行程与成员(tripDate / headcount / memberOrderIds / memberOrderCount) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"remark": "9/24 换 19 座",
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2026-10-08",
|
||||
"serviceEndDate": "2026-10-10",
|
||||
"seats": 19,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "含高速费",
|
||||
"days": [
|
||||
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] },
|
||||
{ "tripDate": "2026-10-09", "headcount": 9, "memberOrderIds": ["2102692937378992129"] },
|
||||
{ "tripDate": "2026-10-10", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "2102750063359135746",
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"status": "DRAFT",
|
||||
"version": 4,
|
||||
"remark": "9/24 换 19 座",
|
||||
"confirmedBy": null,
|
||||
"confirmedAt": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": "2102750063363330049",
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"vehicleTypeName": "大巴系列",
|
||||
"serviceStartDate": "2026-10-08",
|
||||
"serviceEndDate": "2026-10-10",
|
||||
"seats": 19,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "含高速费",
|
||||
"totalSeatCount": 19,
|
||||
"maxHeadcount": 9,
|
||||
"remainingPassengerSeats": 9,
|
||||
"days": [
|
||||
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"], "memberOrderCount": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 整团没有在团需车户时 `groups: []` 是合法提交,响应 `groups` 为空数组。
|
||||
- 车队车型字典(含座位档位)取不到时,**保存口 fail-closed**:抛 809120「车队车型字典暂不可用,无法校验车型,请稍后重试」,本次提交零写入。
|
||||
- `vehicleTypeName` 仍可降级为 null(只影响展示),不影响本单校验。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809124,
|
||||
"message": "第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **报文字段**:{0}=组号(`groupCode`,不是序号)、{1}=本次提交的座位数、{2}=车型大类 key、{3}=该大类的可选档位(升序、方括号包裹)。一次只抛一条,按校验遍历顺序收集。
|
||||
- **校验顺序**:座位档位(809124 / 809120)→ 半填(809118)→ 容量不足(809116)。**档位不可得(报 809120)时不会吞掉同组的半填与容量判据**,仍会继续判下去,避免「先提示稍后重试、补完车辆数才被告知半填」的来回。
|
||||
- **存量分组(seats 与 count 皆 null)整条跳过**:不查字典、不报 809124/809120/809116。这是为了让「不带 fleet 结构再 PUT 一次」的历史团期不会被一刀拦死。
|
||||
- **只填了车辆数(seats 为 null)时不判档位**:交给 809118 报半填,不会编出一条「座位数不在档位内」。
|
||||
- **该车型大类在车队没有任何型号**(`seatOptions` 为空)按「档位不可得」处理 → 809120,需先去车务补型号。
|
||||
- **注意与子订单级错误码不同**:子订单级同一件事是 582024,团级是 809124;两条链路的报文与码值刻意分开(团级报文必须带组号,一份十几组的整份提交被拒后要能定位是哪一组)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
编辑弹窗点「自动汇总」时调用,拿到 `draft` 灌进表单、原样或编辑后 PUT 保存;四个(本次起为五个)诊断字段必须一并展示。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键(不是产品班期 ID) |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期聚合主键 |
|
||||
| currentStatus | String | 当前正式需求状态;未形成时为 null。只有 null 或 DRAFT 能保存 |
|
||||
| draft | Object | 与保存请求体同形,可原样 PUT;`groupId` 恒为 null |
|
||||
| draft.groups[].groupCode | String | 后端生成的组号(车型大写 + 拆组序号:BUS、BUS2…),**前端只读展示,不要给输入框** |
|
||||
| draft.groups[].vehicleType | String | 归一后的车型大类 key(suv2 → suv) |
|
||||
| draft.groups[].seats | Integer | **本次起为档位内的值**:子订单报的座位在档位内取组内最大,不在档位内或没报则兜底该大类最大档;档位不可得时退回旧规则(取子订单报的最大值) |
|
||||
| draft.groups[].count | Integer | `ceil(本组最忙那天的人数 / (seats − 1))`,已扣司机座;seats < 2 时与 seats 一起为 null |
|
||||
| draft.groups[].days[] | Array | 逐日明细,正好铺满本组首日到末日(headcount 为该日实时人数之和) |
|
||||
| droppedFleetItems[] | Array | 没进草稿的车型项(多车型户只进主车型);**必须提示** |
|
||||
| staleHeadcountOrders[] | Array | 实时人数 ≠ 子订单冻结人数的户 |
|
||||
| paddedOrderDays[] | Array | 为过 809109 补进分组的日期 |
|
||||
| **seatOptionAdjusted[]** | Array | **本次新增**:座位档被兜底调整的户,见下 |
|
||||
| seatOptionAdjusted[].groupCode | String | 该户所在的草稿分组编码(同户同车型被拆成 BUS/BUS2 时用它区分) |
|
||||
| seatOptionAdjusted[].orderId / orderNo | String / String | 所属子订单(雪花 ID 按字符串处理) |
|
||||
| seatOptionAdjusted[].vehicleType | String | 车型大类(草稿里该组的 vehicleType) |
|
||||
| seatOptionAdjusted[].originalSeats | Integer / null | 子订单报的单车座位数;没报时为 null |
|
||||
| seatOptionAdjusted[].adoptedSeats | Integer | 草稿实际采用的座位数(该组档位内的值) |
|
||||
| seatOptionAdjusted[].seatOptions | Integer[] | 该大类的可选档位(升序) |
|
||||
| seatOptionAdjusted[].reason | String | `SEATS_NOT_IN_OPTIONS` / `SEATS_MISSING` / `SEATS_MISSING_ADOPTED_GROUP_VALUE` |
|
||||
| violations[] | Array | 草稿预检违规(与保存同一份校验内核):code / reason / detail / groupCode / tripDate / orderId |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"currentStatus": null,
|
||||
"draft": {
|
||||
"version": null,
|
||||
"remark": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2026-10-08",
|
||||
"serviceEndDate": "2026-10-10",
|
||||
"seats": 19,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "HL20260924001: 备注原文",
|
||||
"days": [
|
||||
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"seatOptionAdjusted": [
|
||||
{
|
||||
"groupCode": "BUS",
|
||||
"orderId": "2102692937378992129",
|
||||
"orderNo": "HL20260924001",
|
||||
"vehicleType": "bus",
|
||||
"originalSeats": 13,
|
||||
"adoptedSeats": 19,
|
||||
"seatOptions": [12, 15, 16, 19],
|
||||
"reason": "SEATS_NOT_IN_OPTIONS"
|
||||
}
|
||||
],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 草稿没有可汇总内容时 `draft.groups` 为空数组,`seatOptionAdjusted` 为空数组,接口仍 200。
|
||||
- 有需车户还没提交行程用车需求时报 809121 并逐户列出(不是空草稿)。
|
||||
- **某车型大类在车队没有型号时,汇总口不 fail-closed**:退回旧规则(取子订单报的座位)、不出 `seatOptionAdjusted`,同时在 `violations` 里给出 809120,让页面能提示「这类车型还没在车务建档」。整张字典不可用(Feign 降级)仍直接抛 809120。**保存口对同一情形是 fail-closed**——这条差异是有意的:汇总是只读草稿,卡住它等于管理员连看都看不到。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "seatOptionAdjusted": [], "violations": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 2102749115823919105 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:HL20260924001(2102692937378992129):未提交行程用车需求、HL20260924002(2102692937378992130):车型均不在车型字典内",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `reason` 的三种取值对前端提示语不同:`SEATS_NOT_IN_OPTIONS` = 「你报的 N 座这台车没有,已改用 M 座」;`SEATS_MISSING` = 「这户没报座位,跟字典最大档 M 座走」;`SEATS_MISSING_ADOPTED_GROUP_VALUE` = 「这户没报座位,采用的值 M 来自同组其它户报的合法档位(不是字典最大档)」。**不要把后两者渲染成「系统按字典给你选了 M 座」**。
|
||||
- 座位是**单车**属性,多户报的值不相加(沿用 #8220)。
|
||||
- 拆组逻辑不变:同车型按连续日期段拆组,第二段起组号加序号;诊断行按所属组带 `groupCode`。
|
||||
- `draft.groups[].seats` 即使已被兜底到档位内,**保存时仍会按新校验复核**(同一条 809124);本端点不替代保存侧校验。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
- 座位下拉的数据源是 `GET /admin/fleet/vehicle-types/list` 返回项里的 `seatOptions`(该大类下型号 `seats` 去重升序),**按 `typeKey` 取**(SUV 这类在库里的 typeKey 可能是 `suv2`,返回的 `vehicleType` 是归一后的 `suv`,见 23_8221)。**不要在前端写死档位枚举**。
|
||||
- 切换车型时必须同步换档位并清空旧值,否则旧档位会带着新车型一起提交 → 809124。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照(分组元素)
|
||||
|
||||
```json
|
||||
// ✅ 大巴档位 12/15/16/19 中取值,seats 与 count 成对
|
||||
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 19, "count": 1 }
|
||||
|
||||
// ❌ 13 不在大巴档位内 → 809124
|
||||
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 13, "count": 1 }
|
||||
|
||||
// ❌ 只填座位数、车辆数漏填 → 809118(不是 809124)
|
||||
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 19 }
|
||||
|
||||
// ✅ 存量分组形态:两列都不传,整条跳过档位与容量校验
|
||||
{ "groupCode": "BUS", "vehicleType": "bus" }
|
||||
```
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 保存成功后响应 `version` 会 +1,下次保存必须回传新值,否则 809102。
|
||||
- 「自动汇总」会整份覆盖当前编辑内容,前端须二次确认后再灌入。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无表结构变更、无 Flyway。
|
||||
- 809124 / 809120 都在写库之前抛出:**整份零写入**(校验失败时响应 `data` 为 null,可回读同一份数据确认未变)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 存量分组不批量重算、不迁移:只有它下次出现在某次 PUT 的 `groups` 数组里才会按新校验复核(两列皆 null 时仍整条跳过)。
|
||||
- 同一份提交里多个组同车型时,字典只查一次(不是每组一次 Feign)。
|
||||
- 车队车型字典服务超时/降级:保存口 fail-closed 报 809120;汇总口退回旧座位规则并在 `violations` 里报 809120。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
### 错误码
|
||||
|
||||
| 码 | 触发 | 报文 |
|
||||
|---|---|---|
|
||||
| 809124 | 该组 `seats` 不在该组 `vehicleType` 的可选档位内 | 第 {0} 组的座位数 {1} 不在车型 {2} 的可选档位 {3} 内,请从下拉项中选择 |
|
||||
| 809120(复用) | 车队车型字典不可用 / 该大类在车队没有型号(档位不可得) | 车队车型字典暂不可用,无法校验车型,请稍后重试 |
|
||||
| 809118(不变) | `seats` 与 `count` 只填了一个 | 第 {0} 组的座位数与车辆数必须同时填写,或同时留空 |
|
||||
| 809116(不变) | 扣司机座后可载客座位少于该组最大单日人数 | 第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人 |
|
||||
|
||||
### seatOptionAdjusted[].reason
|
||||
|
||||
| 取值 | 含义 | 建议提示语 |
|
||||
|---|---|---|
|
||||
| `SEATS_NOT_IN_OPTIONS` | 子订单报了座位但不在该大类档位内 | 该户报的 {originalSeats} 座本车型没有,草稿改用 {adoptedSeats} 座(可选 {seatOptions}) |
|
||||
| `SEATS_MISSING` | 子订单没报座位,草稿按字典最大档兜底 | 该户没报座位,草稿按 {adoptedSeats} 座(字典最大档)计 |
|
||||
| `SEATS_MISSING_ADOPTED_GROUP_VALUE` | 子订单没报座位,草稿采用的值来自同组其它户报的合法档位 | 该户没报座位,草稿跟随本组 {adoptedSeats} 座计 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 保存接口请求/响应字段 | — | 无新增、无删除,结构不变 |
|
||||
| `aggregate-draft` 响应 | 无 `seatOptionAdjusted` | 新增 `seatOptionAdjusted[]`(8 个子字段) |
|
||||
| `draft.groups[].seats` | 组内子订单报的最大单车座位 | 该大类档位内的值(在档位内取组内最大、否则兜底最大档;档位不可得退回旧规则) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 提交字典内档位(如 bus 19 座) | 放行 | 放行(不变) |
|
||||
| 提交字典外档位(如 bus 13 座) | **放行** | **809124 拒绝** |
|
||||
| 提交座位数但车型大类在车队没有型号 | 放行 | 809120 拒绝(保存口);汇总口退回旧规则 + violations 里报 809120 |
|
||||
| 存量分组(seats/count 皆 null)整份再提交 | 跳过座位相关校验 | 不变,仍整条跳过 |
|
||||
| 预检/整团确认侧 | 不校档位 | 不变(该入口读的是库里已存值,且被 `doConfirm` 在事务内调用,见「七、不影响范围」) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:请求/响应结构不变,但**同一份 payload 可能从 200 变成 809124** —— 凡是座位数不在该大类档位内的提交(含手输、含车型切换后未更新座位数)都会被拒。809124 是 HTTP 200 下的业务失败。
|
||||
- **前端是否必须同步上线**:**必须**。座位数必须改成档位下拉(否则运营按老习惯手输就会撞 809124);`seatOptionAdjusted` 必须展示(否则草稿改座位这件事不可见);组号输入框应改成只读展示。
|
||||
- **存量数据影响**:不批量重算、不迁移。库里的历史非法档位只会在下一次保存/汇总时暴露:保存被 809124 拒(需管理员改成档位内值);预检与整团确认口**不**重判档位,不会把已确认流程卡死。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`PUT .../vehicle-requirement` 的座位档位校验(新增 809124);`GET .../vehicle-requirement/aggregate-draft` 的 `seats` 取值与新增 `seatOptionAdjusted`。
|
||||
- **零影响**:
|
||||
- 两个接口的请求/响应字段结构(除新增 `seatOptionAdjusted`)。
|
||||
- 既有错误码 809116 / 809118 / 809117 / 809119 / 809120 / 809121 / 809102 等其余判据与报文。
|
||||
- `GET .../vehicle-requirement`(读取回显)不触发校验,原样返回库里已落值。
|
||||
- **整团确认预检(`GET .../requirement/confirm-check`)与 `confirm` 写口不重判座位档位**:它们读的是库里已存值,且确认链路在事务内,不在那里调字典 Feign。即「历史非法档位不会卡住确认」,只会在下次编辑保存时要求修正。
|
||||
- `GET .../requirement/vehicle-households`、`GET .../requirement-summary` 未改动。
|
||||
- 子订单级用车需求(582024 那条)未改动。
|
||||
- fleet 服务与网关:零改动(`/admin/fleet/vehicle-types/list` 复用既有只读端点)。
|
||||
- 数据库:无表变更、无 Flyway。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署读数**(测试服 `deploy-status.sh`,2026-09-24 12:07 读取):
|
||||
|
||||
```text
|
||||
SERVICE BRANCH COMMIT BEHIND DEPLOYED_AT STATE
|
||||
hl-order-service-v3 dev-v3 1638faf08 0/N 2026-09-24 12:06:26 ok
|
||||
hl-gateway dev-v3 c238f38c3 182/Y 2026-09-21 14:44:55 ok
|
||||
hl-fleet-service dev-v3 cc70c9ba3 6/N 2026-09-24 10:49:54 ok
|
||||
```
|
||||
|
||||
被测服务 `BEHIND=0/N STATE=ok`;请求路径上的 `hl-gateway` 与断言依赖的 `hl-fleet-service` 同为 `ok`,两行的 BEHIND 右侧字母均为 `N`(落后提交未触及这两个服务本身)。
|
||||
|
||||
**网关真实请求与响应**(`https://api.test.1814.love`,`groupBatchId=2102692937584513025`,当前 DRAFT v3:BUS 16 座 ×1、SUV 5 座 ×1):
|
||||
|
||||
```text
|
||||
① 原值提交 BUS 16 座 / SUV 5 座(均在档位内)
|
||||
PUT /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement
|
||||
→ http 200, code=200, message=成功;回读 version 2→3,seats 仍为 16/5 ✓
|
||||
|
||||
② BUS 提交 13 座(bus 档位 12/15/16/19)
|
||||
→ http 200, code=809124
|
||||
message=第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择 ✓
|
||||
拒绝后回读:version 仍为 3、seats 仍为 16/5、remark 未变(整份零写入)✓
|
||||
|
||||
③ SUV 提交 6 座(suv 档位 5/7)
|
||||
→ http 200, code=809124
|
||||
message=第 SUV 组的座位数 6 不在车型 suv 的可选档位 [5, 7] 内,请从下拉项中选择 ✓
|
||||
|
||||
④ GET /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement/aggregate-draft
|
||||
→ code=200;draft.groups = BUS(bus, 16 座, 1 辆, 3 天) + SUV(suv, 5 座, 1 辆, 3 天)
|
||||
seatOptionAdjusted = [](该团期子订单报的座位都合法,无兜底调整)
|
||||
violations = [];droppedFleetItems 1 条、stale/padded 0 条 ✓
|
||||
```
|
||||
|
||||
**定向测试逐类读数**(`mvn -o -pl hl-order-service-v3 -am test -Dtest='…' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,14 个类合计 0 failures / 0 errors / 0 skipped,BUILD SUCCESS):
|
||||
|
||||
| 测试类 | Tests run |
|
||||
|---|---|
|
||||
| GroupVehicleRequirementSaveTest | 31 |
|
||||
| GroupVehicleDraftAggregatorTest | 24 |
|
||||
| GroupVehicleRequirementAggregateDraftTest | 12 |
|
||||
| GroupVehicleRequirementConfirmCheckTest | 19 |
|
||||
| GroupVehicleRequirementLockContractTest | 3 |
|
||||
| GroupBatchRequirementServiceTest | 73 |
|
||||
| GroupBatchRequirementServiceConfirmVehicleTest | 17 |
|
||||
| GroupBatchRequirementServiceCheckVehicleTest | 17 |
|
||||
| GroupBatchRequirementConfirmReflectionGuardTest | 4 |
|
||||
| FleetVehicleTypeNameLoaderTest | 9 |
|
||||
| TransactionalRemoteCallArchTest | 1 |
|
||||
| ErrorCodeUniquenessGuardTest | 3 |
|
||||
| RequirementGroupBatchErrorCodeRangeTest | 4 |
|
||||
| ErrorCodeTemplateNumberFormattingTest | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 自动汇总草稿端点:#8220 → `changelogs-v2/2026-09/23_8220_团期正式行程用车需求自动汇总草稿-新增接口-管理后台.md`
|
||||
- 车辆规格四字段(seats/count/specialTags/remark):#8152 → `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md`
|
||||
- 座位充足性校验扣司机座(809116):#8278 → `changelogs-v2/2026-09/23_8278_团级用车分组座位校验扣司机座-修改接口-管理后台.md`
|
||||
- 座位数下拉先例(子订单级):#4856 / #4871 → `changelogs-v2/2026-07/50_4856_订单调整用车座位数按车型型号下拉-管理后台.md`
|
||||
- 车型大类字典与 SUV 回显归一:#8202 / #8221 → `changelogs-v2/2026-09/`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- Issue: https://git.1814.love/wx/HL/issues/8311
|
||||
- 后端 PR: 待建
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: jw / wx
|
||||
@@ -0,0 +1,445 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8320"
|
||||
title: "调整订单:团期子订单未录大交通时提交用车需求须显式确认「不用接送机」(新错误码 587044)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "f8c389c1a1230316163bac80079675fb3c1a26ad"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "已合 dev-v3(PR #8326,bc197bc07)并部署测试服,deploy-status.sh 回读 hl-order-service-v3=bc197bc07、BEHIND 0/N、STATE ok。网关实测五组全过:①无大交通不带声明提交团期单车辆需求 → 587044 且整笔零写入(order_main/需求/调整记录/状态日志前后快照逐项相等)②两方向声明 true + 同一份 vehicleRequirement → 200,order_main 两列置 1,snapshot 回显 transferDeclaration=true/true、transferTransportPresent=false/false ③该方向补大交通批次后声明不生效,车务读侧仍 required=true/READY<接客信息已齐> ④同一张单同一时刻:已声明不用且无批次的方向 required=false/NOT_REQUIRED/<无需送客>,未声明且无批次的方向 required=true/MISSING/<待补接客信息>(阴性对照)⑤接机有批次、送机无批次时提交非空 transferRequirement 未声明送机 → 587044<不用送机>且零写入;散客单同 payload 不带声明 → 200 正常落库(口径 4 成立)。定向单测 14 类 386 例全绿 + ArchUnit/错误码门禁绿。 | 2026-09-24 mmg 交付:调整订单车辆安排页新增「接送机确认」卡(仅团期子订单+该方向无大交通渲染不用接机/送机必选确认,有批次不渲染),snapshot 回显声明+基线已声明无批次锁定禁撤销,前置闸报文与 587044 逐字一致本地拦,提交整份回传 transferDeclaration 自动弃快捷写口走统一提交;FunItemAdjustModal spec 46 例全绿(新增 6 例)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 调整订单:团期子订单未录大交通时提交用车需求须显式确认「不用接送机」
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8326 | **Issue**: #8320 | **合并提交**: `bc197bc07`
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「订单详情 → 调整订单 → 车辆安排」。**仅团期子订单**受影响,散客单行为完全不变。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**接送机那一槽多了一条硬闸,前端不做就会撞 587044 整笔提交失败。**
|
||||
|
||||
1. **团期子订单**(下单时带 `productBatchId` 的订单)在「车辆安排」页提交时,如果**某个方向一条大交通批次都没有**,该方向就必须在提交体里显式确认「不用接送机」(`true`),否则整笔被拒,返回 **587044**,**一个字都不落库**。有批次的方向不需要确认(沿用大交通,声明对它无效)。两个方向**各自判定**。
|
||||
2. 确认之后,**车务侧那一格的文案会变**:该方向由「待补接客信息 / 待补送客信息」变成「**无需接客 / 无需送客**」。不确认则维持「待补」——这是有意的,不要把「客人还没填大交通」读成「不用接送机」。
|
||||
3. **要接送机的路径没有变**:需要接送机就必须先把大交通录进去(接送机用车需求的服务日只能从大交通航班日派生)。本次没有新增「需要但航班未定」这种第三态。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
「车辆安排」页的接送机用车槽,服务日由服务端从大交通批次派生。客人没录大交通时:
|
||||
|
||||
- 订单侧算出来的接送要求是「未知」,车务侧按「需要」处理,于是看板/配车页**恒显示「待补接客信息 / 待补送客信息」**;
|
||||
- 而车务侧的接送机状态里本来就有「无需接客 / 无需送客」这一档,**订单侧却没有任何入口能把它置上**。
|
||||
|
||||
结果是「客人还没填大交通」与「这单就是不用接送机」在数据上完全一样,接送机缺口永远关不掉。本版增加一个**订单级的显式声明**来承载「就是不用」这个语义,并在提交侧设闸,逼定制师傅表态。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 调整订单预填快照查询 | GET | `/v3/admin/order/{id}/adjustment/snapshot` | 修改接口 | 出参新增 `transferDeclaration` 与 `transferTransportPresent` |
|
||||
| 2 | 调整订单统一提交 | POST | `/v3/admin/order/{id}/adjustment/submit` | 修改接口 | `updates` 新增 `transferDeclaration`;某方向无大交通未表态时返回新码 **587044** |
|
||||
|
||||
读侧(车务看板 / 团期配车)**路径与响应结构均未变**,只有接送机那一档的取值会随声明变化,见「六.6、修改前后对比」。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 调整订单预填快照查询 `GET /v3/admin/order/{id}/adjustment/snapshot`
|
||||
|
||||
**VO**: `AdjustmentSnapshotRespVO`(入参为 path 变量 + query,无独立 ReqVO)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台打开「调整订单」弹窗时调用一次,一次性拉取各子领域当前值用于回显。本次相关的是「车辆安排」页:需要知道(a)接送机不用声明的当前值,(b)两个方向**是否已有大交通批次**——后者决定前端要不要渲染「不用接送机」的必选确认。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| id | Path | Long | ✅ | 订单雪花 ID(**字符串传输,别转 Number**) | 订单 ID |
|
||||
| scope | Query | String | ❌ | 逗号分隔:`BASIC`/`PEOPLE`/`SCHEDULE`/`ITINERARY`/`HOTEL_REQ`/`VEHICLE_REQ`;任一 token 非法返 587003 | 限定返回子领域;不传返全部。**传了但不含 `VEHICLE_REQ` 时,本次两个新字段不返回** |
|
||||
|
||||
#### 出参 `Result<AdjustmentSnapshotRespVO>`
|
||||
|
||||
本次**新增**字段(其余字段结构与取值规则一律不变):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `transferDeclaration.pickupNotRequired` | Boolean | **新增**。接机方向是否已声明「本单不用接机」。**未声明返回 `false`**(不是 null) |
|
||||
| `transferDeclaration.dropoffNotRequired` | Boolean | **新增**。送机方向是否已声明「本单不用送机」。未声明返回 `false` |
|
||||
| `transferTransportPresent.pickup` | Boolean | **新增**。接机方向**是否已存在大交通批次**。`true` 时该方向不需要「不用接送机」确认,且声明对它不生效 |
|
||||
| `transferTransportPresent.dropoff` | Boolean | **新增**。送机方向是否已存在大交通批次 |
|
||||
|
||||
其余字段(`vehicleRequirement` / `transferRequirement` / `vehicleTransportSummary` / `basic` 等)**结构与取值均未变**。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/2101219133700952066/adjustment/snapshot?scope=VEHICLE_REQ
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"vehicleRequirement": { "...": "结构未变,略" },
|
||||
"transferRequirement": null,
|
||||
"vehicleTransportSummary": {
|
||||
"hasPickupTime": false,
|
||||
"displayText": null,
|
||||
"emptyText": "暂无接送机时间",
|
||||
"arrivals": [],
|
||||
"departures": []
|
||||
},
|
||||
"transferDeclaration": { "pickupNotRequired": false, "dropoffNotRequired": false },
|
||||
"transferTransportPresent": { "pickup": false, "dropoff": false }
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 订单没有大交通批次:`vehicleTransportSummary.hasPickupTime=false`、`emptyText="暂无接送机时间"`、`arrivals/departures` 均为 `[]`;`transferTransportPresent` 两个方向都是 `false`。**这是正常态,不是降级**。
|
||||
- 订单从未表过态:`transferDeclaration` 两个字段都是 `false`(表示「未声明」,不是「声明了需要」)。
|
||||
- **老订单 / 存量数据**:两列默认 0,读出来与「从未表态」完全一致,不会异常。
|
||||
- `scope` 不含 `VEHICLE_REQ`:本组三个字段(含本次两个)整体不返回。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 587003, "message": "调整范围取值非法", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只读接口,无副作用;不因订单状态(已出行、已结算)而拒绝。
|
||||
- `transferTransportPresent` 判的是「该方向有没有大交通批次」,**不看**批次的 `pickupRequired`;与提交闸、车务读侧回落同一句判据。
|
||||
- 订单 ID 传错/不存在 → 既有 581007「订单不存在」,本次未改。
|
||||
|
||||
---
|
||||
|
||||
### 2. 调整订单统一提交 `POST /v3/admin/order/{id}/adjustment/submit`
|
||||
|
||||
**VO**: `AdjustmentSubmitReqVO` → `AdjustmentSubmitRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「调整订单」弹窗内收集齐所有子领域改动后一次性提交;「车辆安排」页保存行程用车 / 接送机用车需求、以及本次新增的接送机不用声明,都走这一个入口。单事务原子应用:任一步失败整笔回滚。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| id | Path | Long | ✅ | 订单雪花 ID | 订单 ID |
|
||||
| `updates` | Body | Object | ✅ | 各子领域容器,不可全 null | 未改的子领域置 null |
|
||||
| `updates.transferDeclaration` | Body | Object | ❌ | **本次新增** | 接送机不用声明,见下 |
|
||||
| `updates.transferDeclaration.pickupNotRequired` | Body | Boolean | ❌ | `true`=本单不用接机;`false`=需要(或撤销声明) | 接机方向表态 |
|
||||
| `updates.transferDeclaration.dropoffNotRequired` | Body | Boolean | ❌ | `true`=本单不用送机 | 送机方向表态 |
|
||||
| `updates.vehicleRequirement` | Body | Object | ❌ | 结构未变(`fleet` 为空数组按「未提交」处理) | 行程用车需求 |
|
||||
| `updates.transferRequirement` | Body | Object | ❌ | 结构未变;服务日由服务端派生 | 接送机用车需求 |
|
||||
| 其余 `updates.*` | Body | Object | ❌ | 全部未变 | 出行人 / 改期 / 行程 / 房需求 |
|
||||
|
||||
#### 出参 `Result<AdjustmentSubmitRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `success` | Boolean | 恒 `true`(失败走错误码) |
|
||||
|
||||
结构未变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无大交通、只提交行程用车、**不带声明**(会被拒,用于说明闸的位置):
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"vehicleRequirement": {
|
||||
"specialTags": [],
|
||||
"remark": "按原方案",
|
||||
"fleet": [ { "vehicleType": "SUV系列", "seats": 5, "count": 1 } ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
两个方向都确认「不用」:**这是无大交通时唯一能被接受的形态**
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"vehicleRequirement": {
|
||||
"specialTags": [],
|
||||
"remark": "按原方案",
|
||||
"fleet": [ { "vehicleType": "SUV系列", "seats": 5, "count": 1 } ]
|
||||
},
|
||||
"transferDeclaration": { "pickupNotRequired": true, "dropoffNotRequired": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
只改声明、不动其他子领域(有效提交,不会被 587033 拦):
|
||||
|
||||
```json
|
||||
{ "updates": { "transferDeclaration": { "pickupNotRequired": true, "dropoffNotRequired": false } } }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "success", "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- `updates.transferDeclaration` 传空对象 `{}`(两个字段都 null)或整体不传:按「本次没对车辆子领域表态」处理,**不触发 587044**。
|
||||
- 散客单(无 `productBatchId`)传了 `transferDeclaration`:**不写入、不报错**,静默忽略。
|
||||
- 该方向已有大交通批次时传了声明:**不写入、不报错**,接送要求仍按批次 `pickupRequired` 判。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 587044, "message": "本单未录入大交通,请先确认「不用接机」,或先在大交通模块录入行程后再提交", "data": null, "success": false }
|
||||
```
|
||||
|
||||
(方向占位符取「接机」或「送机」,两个方向各自判定,只报先撞上的那一个。)
|
||||
|
||||
```json
|
||||
{ "code": 587033, "message": "未检测到有效变更,无需提交", "data": null, "success": false }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **只对团期子订单生效**:散客单行为与本次之前逐字相同。
|
||||
- **只在本次提交真的带了车辆子领域有效载荷时判定**:非空 `fleet`、或 `transferDeclaration` 任一方向非 null。改房务、改行程、加出行人、改期这些提交**不会被 587044 拦**。
|
||||
- **整笔零写入**:587044 与其它守卫一样在任何 DB 写之前抛出,不存在「声明写了、需求没写」的半截状态。
|
||||
- **覆盖写 + 值级 no-op**:字段为 null 表示「本次不动这个方向」;两个方向的值与现值都相同时不写库、不产调整记录项。
|
||||
- **两个方向独立**:可以只声明接机不用、送机照旧(送机若已补录大交通则不受影响)。
|
||||
- 幂等/并发:沿用本接口既有的订单级锁与「提交期间订单被改」守卫(587043),本次未改。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写**后端接受 / 拒绝 payload 的规则**。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
前提:订单是**团期子订单**,且两个方向**都没有大交通批次**。
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|---|---|---|
|
||||
| ✅ 只提交行程用车 + 两个方向都确认不用 | `{"updates":{"vehicleRequirement":{"fleet":[{"vehicleType":"SUV系列","seats":5,"count":1}]},"transferDeclaration":{"pickupNotRequired":true,"dropoffNotRequired":true}}}` | 200 |
|
||||
| ✅ 只提交声明(不动车辆) | `{"updates":{"transferDeclaration":{"pickupNotRequired":true,"dropoffNotRequired":true}}}` | 200 |
|
||||
| ❌ 只提交行程用车、不带声明 | `{"updates":{"vehicleRequirement":{"fleet":[...]}}}` | **587044** |
|
||||
| ❌ 只声明了一个方向(另一个方向无批次) | `{"updates":{"vehicleRequirement":{"fleet":[...]},"transferDeclaration":{"pickupNotRequired":true,"dropoffNotRequired":null}}}` | **587044**(报「送机」) |
|
||||
| ✅ 改房务 / 改行程 / 改期(**完全不带车辆子领域字段**) | `{"updates":{"hotelRequirement":{...}}}` | 200(不判 587044) |
|
||||
| ⚠️ 混方向:接机**有**批次、送机**没有** | `{"updates":{"transferRequirement":{"fleet":[...]}}}`(未声明送机) | **587044**——即便接送机需求能派生出服务日也一样拦,防「带着没表过态的方向落库」 |
|
||||
| ✅ 散客单不带声明 | `{"updates":{"vehicleRequirement":{"fleet":[...]}}}` | 200(散客单不判) |
|
||||
| ✅ 该方向已有大交通批次 | 传或不传声明 | 200,声明不生效 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- `transferDeclaration` 是**逐方向覆盖写**:只传想动的方向,另一个方向传 `null` 或干脆不传(**不要**因为「表单里没这个控件」就传 `false`,那会被当成「本方向需要接送机」,在无批次时被 587044 拒绝)。
|
||||
- **未进入 / 未编辑「车辆安排」页时,整个 `transferDeclaration` 都不要回传**(连空对象也建议不传)——只要出现 `true`/`false` 就视为对车辆子领域表态。
|
||||
- 需要接送机时不要试图用声明绕过:接送机用车需求的服务日只能来自大交通,先录大交通。
|
||||
- **判据只看本次请求体,不回看库里已存的声明**:某方向无大交通时,本次提交**必须带上该方向的表态**。前端如果只回传「变化过的字段」(例如只带 `transferRequirement`),即使客人此前已勾过「不用接送机」也会被 587044 挡住。**正确做法:车辆安排页每次提交都把 `transferDeclaration` 按当前控件状态整份回传。**
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
涉及写操作:`order_main` 新增两列,语义为「该方向**已显式声明不用接送机**」。
|
||||
|
||||
| 前端提交 `updates.transferDeclaration` | `pickup_not_required` 列 | `dropoff_not_required` 列 |
|
||||
|---|---|---|
|
||||
| `{"pickupNotRequired":true,"dropoffNotRequired":true}` | `1` | `1` |
|
||||
| `{"pickupNotRequired":true,"dropoffNotRequired":null}` | `1` | 保持原值(不动) |
|
||||
| `{"pickupNotRequired":false,"dropoffNotRequired":false}` | `0` | `0` |
|
||||
| 不传 / 空对象 | 保持原值 | 保持原值 |
|
||||
|
||||
- 列类型 `TINYINT(1) NOT NULL DEFAULT 0`;**存量数据不做任何回填**,全部为 0(未声明)。
|
||||
- 该方向**已有大交通批次**时即使传了 `true` 也**不写库**(声明对该方向不生效)。
|
||||
- 提交成功时同时写一条调整记录项(类型为车辆需求类,文案「接送机不用声明已更新」,带方向的中文前后值)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 / 无权限 → 网关拦截(401/403),本次未改。
|
||||
- 订单不存在 → 既有 581007。
|
||||
- 订单已是终态 → 既有 587002「订单已是终态,不可调整」。
|
||||
- 提交期间订单被并发改动 → 既有 587043,重新拉取后重试。
|
||||
- 老前端(不认识这两个新字段、也不回传 `transferDeclaration`):**团期子订单只要提交车辆子领域就会被 587044 拦住**——这是有意的硬闸,前端必须同步上线。
|
||||
- 老数据兼容:存量订单两列为 0,读侧与「从未表态」一致;车务侧该方向仍显示「待补」,不会被静默读成「无需接送」。
|
||||
- 大交通批次 `direction` 为空的历史行:与订单侧既有的方向切分口径一致(该行不归入任何方向)。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### 接送机 readiness(车务读侧)
|
||||
|
||||
**所属字段**: 车务看板 / 配车页 `readiness.statusCode`(本次**未改路径与结构**,仅取值可能变化)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `NOT_REQUIRED` | 无需接客 / 无需送客 | 该方向**有**大交通批次且批次 `pickupRequired=false`,**或**该方向无批次但订单已声明「不用接送机」 |
|
||||
| `MISSING` | 待补接客信息 / 待补送客信息 | 该方向需要接送但信息不齐;**无大交通且未声明也算这一档**(fail-closed) |
|
||||
| `PARTIAL` | 接客/送客信息部分缺失 | 有多条批次,部分缺时刻或站点 |
|
||||
| `READY` | 接客/送客信息已齐 | 全部批次时刻与站点齐全 |
|
||||
| `SOURCE_UNAVAILABLE` | 订单信息暂不可用 | 订单上下文取数失败(降级态) |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| snapshot 出参 `transferDeclaration` | 不存在 | `{pickupNotRequired:Boolean, dropoffNotRequired:Boolean}`,未声明为 `false` |
|
||||
| snapshot 出参 `transferTransportPresent` | 不存在 | `{pickup:Boolean, dropoff:Boolean}` |
|
||||
| submit 入参 `updates.transferDeclaration` | 不存在 | 新增,两个方向可选 |
|
||||
| `updates` 其余字段 / 出参结构 | - | 未变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 团期子订单无大交通、只提交行程用车 | 200,接送机不留任何结论 | **587044**,整笔零写入 |
|
||||
| 团期子订单无大交通、确认两个方向不用后提交 | 无此路径 | 200,订单落两列声明 |
|
||||
| 车务侧某方向(无大交通) | 恒「待补接客信息 / 待补送客信息」 | 已声明 ⇒「无需接客 / 无需送客」;未声明仍「待补」 |
|
||||
| 车务侧某方向(有大交通批次) | 按批次 `pickupRequired` 判 | **不变** |
|
||||
| 散客单 | - | **完全不变** |
|
||||
| 809002 / 809009 | 接送机用车需求的服务日与开关校验 | **触发条件均未改**(但无大交通且未表态时,会先撞 587044) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否(纯新增字段;但**行为上对团期子订单是硬闸**——老前端提交车辆子领域会撞 587044)
|
||||
- **前端是否必须同步上线**: **是**(团期子订单的「车辆安排」页必须按 `transferTransportPresent` 渲染必选确认并回传 `transferDeclaration`)
|
||||
- **前端 workaround 清理点**: 无
|
||||
|
||||
---
|
||||
|
||||
## 六.8、已知约束(实测确认,非缺陷,但前端要知道)
|
||||
|
||||
1. **声明的撤销只在「该方向已有大交通批次」时可达**。该方向一条批次都没有时,把声明清回 `false` 会被 587044 拒绝(实测:两方向都清 `false` → 587044「不用接机」;只清送机、接机留 `true` → 587044「不用送机」)。要改回「需要接送机」,先录该方向的大交通——这也是接送机服务能落地的必经步骤。
|
||||
2. **团期子订单的用车需求提交后是「待审核」,不即时进车务池**:需由团期管理员执行提交车务动作后才会出现在车务看板。这不是本版引入的行为,但会影响「提交完立刻看车务看板」的验收动作。
|
||||
3. 大交通批次新增接口有 3 秒幂等窗口,同一订单连续两次新增会返回 100502「大交通批次新增处理中」,与本次改动无关。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台「订单详情 → 调整订单 → 车辆安排」的团期子订单提交路径;以及车务侧接送机 readiness 的取值。
|
||||
- **零影响**:
|
||||
- 散客单(无 `productBatchId`)的一切调整行为
|
||||
- 「调整订单」的其余 tab(出行人 / 改期 / 行程 / 房需求)
|
||||
- 接送机用车需求(TRANSFER)的服务日派生、提交开关、809002 / 809009 的触发条件
|
||||
- 大交通模块自身的读写接口
|
||||
- 行程单 / H5 / 小程序
|
||||
- 网关路由(无新增,均在既有 `/v3/admin/**` 通配下)
|
||||
- 历史数据(存量两列为 0,不迁移)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
被测服务 `hl-order-service-v3`:`dev-v3 @ bc197bc07`,`deploy-status.sh` 回读 `BEHIND 0/N`、`STATE ok`。
|
||||
|
||||
```
|
||||
被测服务部署:`hl-order-service-v3` = `dev-v3 @ bc197bc07`,`BEHIND 0/N`、`STATE ok`。
|
||||
网关 `hl-gateway` 本次无路由改动;`hl-fleet-service` 现网版本虽落后若干提交,但落后的提交全是 order-v3 的、
|
||||
未触及 fleet 代码,其接送就绪态判据与本次工作树逐字相同,故车务读口读数有效。
|
||||
|
||||
实测用**自建的团期子订单测试单**(客户备注带 `HLTEST[8320]` 标记,未碰任何真实订单):
|
||||
|
||||
```text
|
||||
① 无大交通 + 不带 transferDeclaration 提交行程用车
|
||||
POST /v3/admin/order/{orderId}/adjustment/submit → HTTP 200,code=587044
|
||||
"本单未录入大交通,请先确认「不用接机」,或先在大交通模块录入行程后再提交" ✓
|
||||
零写入核对(提交前后同一次 SQL 快照逐项相等):
|
||||
order_main 两列仍为 0 / order_vehicle_requirement 无新版本 /
|
||||
order_transport_plan 无变化 / order_adjustment_record 空 / order_status_log 计数不变 ✓
|
||||
|
||||
② 同一 payload + transferDeclaration={pickupNotRequired:true,dropoffNotRequired:true}
|
||||
POST .../adjustment/submit → {"code":200,"data":{"success":true}} ✓
|
||||
GET .../adjustment/snapshot?scope=VEHICLE_REQ
|
||||
"transferDeclaration":{"pickupNotRequired":true,"dropoffNotRequired":true}
|
||||
"transferTransportPresent":{"pickup":false,"dropoff":false} ✓
|
||||
order_main: pickup_not_required=1 dropoff_not_required=1 ✓
|
||||
order_adjustment_record: label="接送机不用声明已更新"
|
||||
before="接机:需要接送,送机:需要接送" after="接机:不用接送,送机:不用接送" ✓
|
||||
|
||||
③ 给该方向补一条 ARRIVAL 大交通批次后再提交(仍带声明 true)
|
||||
snapshot "transferTransportPresent":{"pickup":true,"dropoff":false} ✓
|
||||
车务读侧 pickupSummary: required=true, statusCode=READY, "接客信息已齐" ✓(声明不覆盖批次)
|
||||
|
||||
④ 车务读侧阳性 + 阴性对照(同一张单、同一时刻,两个方向都没有批次)
|
||||
pickupSummary : 未声明(0) → required=true, statusCode=MISSING, "待补接客信息" ✓
|
||||
dropoffSummary : 已声明不用(1) → required=false, statusCode=NOT_REQUIRED, "无需送客" ✓
|
||||
|
||||
⑤ 混方向:接机有批次、送机无批次,提交非空 transferRequirement 且不声明送机
|
||||
→ code=587044 "…请先确认「不用送机」…",整笔零写入 ✓
|
||||
|
||||
⑥ 散客单(productBatchId=null)不带声明提交同一份用车需求
|
||||
→ {"code":200,"data":{"success":true}},正常落库;snapshot transferDeclaration={false,false} ✓
|
||||
```
|
||||
|
||||
(车务读侧入口:管理端 `GET /admin/fleet/board/orders`——fleet 管理端路径**没有 `/v3` 前缀**;
|
||||
`/v3/internal/**` 经网关直连返 403 属设计内,未作为证据。)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #8024 | #7443 | 调整订单车辆安排页同页提交行程用车 + 接送机用车两类需求 | ✅ 有效,本版在其上加闸 |
|
||||
| #7439 | #7439 | 用车需求分家(TRAVEL / TRANSFER),接送机服务日由大交通派生 | ✅ 有效 |
|
||||
| #8153 | #8153 | 声明了接送机却无 TRANSFER 需求行的户在确认预检里报出 | ✅ 有效 |
|
||||
| **本 PR #8326** | **#8320** | 补「不需要接送机」这一侧的显式声明 + 提交闸 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8320](https://git.1814.love/wx/HL/issues/8320)
|
||||
- 关联 PR: [wx/HL#8326](https://git.1814.love/wx/HL/pulls/8326)
|
||||
- 团期车务实施单: `HL-v3/docs/group/实施单/06-团期车务.html`(接送机 readiness 口径本次补了一句)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8320](https://git.1814.love/wx/HL/issues/8320)
|
||||
- **PR**: [#8326](https://git.1814.love/wx/HL/pulls/8326)
|
||||
- **Merge commit**: [bc197bc07](https://git.1814.love/wx/HL/commit/bc197bc07)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,338 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8322"
|
||||
title: "团期预支核单前均可发起(放开招募中)+ 领款人限定为本团主报账人(新码 589557);一并补 #8270 放宽说明"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "0ba1a2732022c800b2cee0711f1ed052b5e19dad"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "jw 2026-09-24 定两条口径:① 只要在核单前都可以发起团期预支,允许状态在 #8270(资源准备中起)基础上补招募中;② 团期预支由主报账人申领,领款人限定为本团 reporter_rank=PRIMARY 的那一人,在本团但不是主报账人(含本团未设主报账人)返新码 589557,不在本团仍 585007;领款人候选接口只返回主报账人一条,未设主报账人返回空列表。#8270(2026-09-23 合入,资源准备中放开、四项配齐门 589542 不再返回)当时未推 changelog,本条一并说明。PR #8323 已合 dev-v3(980429ac2)并部署 TEST,2026-09-24 11:47~11:49 经真实网关实测:招募中团期主报账人提交 200/SUBMITTED 落库 1 行;普通人员、次报账人、未设主报账人三种情况各返 589557 且零写入;不在本团返 585007;招募中超池返 585004;核单中、已流团团期各返 589541;候选接口部署前同一团返回 2 人、部署后只返回主报账人。前端侧:候选下拉条数变为 0 或 1,招募中与资源准备中的预支按钮若按旧规则置灰需放开,589557 需给出文案,故 frontend_status 记 pending。 | 2026-09-24 mmg 交付:FinanceTab 阶段门白名单 3 态放宽到 6 态(RECRUITING~TRIP_FINISHED,核单中/已结算/流团置灰+tooltip),GroupAdvanceModal 空候选引导改「请先在团期人员配置中设置主报账人」(提交必然 589557 故禁用),589557 无按码分支拦截器透后端文案即引导;JSDoc/注释按 #8322+#7154 订正(589538/589539→589541,589542 删除);FinanceTab spec 11 例+新建 GroupAdvanceModal spec 2 例全绿"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期预支:核单前均可发起 + 领款人限定为主报账人(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8323(本单)、#8272(#8270,一并补说明)
|
||||
> **Issue**: #8322、#8270
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期详情页「财务」tab 的「发起预支」弹窗(领款人下拉、提交按钮可用状态、错误提示)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
本次是**行为变更**,三条都与前端此前的预期不同:
|
||||
|
||||
1. **可发起预支的团期状态扩大**。#8270 之前只有物料准备中至出行完毕可提,且要求房 / 车 / 导 / 摄四项配齐;#8270 起资源准备中可提、四项配齐门删除;**本单起招募中也可提**。现在的规则是:**进入核单之前都可以发起**,核单中 / 已结算 / 已流团仍返 `589541`。
|
||||
2. **领款人只能是本团主报账人**。以前本团任一已配人员都能当领款人;现在在本团但不是主报账人的(次报账人、普通人员、本团还没设主报账人)一律返新码 **`589557`**。不在本团仍是 `585007`。
|
||||
3. **领款人候选只返回主报账人一条**。以前返回本团全部人员、主报账人排首;现在只返回主报账人,**本团未设主报账人时返回空列表**。前端下拉要能处理 0 条(提示去团期人员配置里设主报账人)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期预支复用订单预支 `order_advance`(#7154,`scope=GROUP_BATCH`)。本期 wx 在做团期主报账人功能,由主报账人发起预支申领,jw 2026-09-24 据此定:领款人限定为本团主报账人;同时预支入口前移到招募中,只要团期还没进核单都可以发起。
|
||||
|
||||
主报账人在团期人员配置里设置(`PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`),招募中、资源准备中可设,确认后锁定(#8231 / #8269)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 发起团期预支(GB-ADM-042) | POST | `/v3/admin/order/group-batch/{groupBatchId}/advance` | 修改 | 允许状态补招募中;领款人须为本团主报账人,否则新码 589557;#8270 起资源准备中可提、589542 不再返回 |
|
||||
| 2 | 团期预支领款人候选 | GET | `/v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates` | 修改 | 只返回主报账人一条;未设主报账人返回空列表 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 发起团期预支 `POST /v3/admin/order/group-batch/{groupBatchId}/advance`
|
||||
|
||||
**VO**: `CreateGroupBatchAdvanceReqVO` → `OrderAdvanceRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「财务」tab 点「发起预支」,选领款人(主报账人)、借款类型、金额后提交,创建一条团期级预支单,直接进入站内财务待审批(`SUBMITTED`)。入参与出参结构均未改,变的是允许状态与领款人校验。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchId | path | Long | 是 | 运营团期 ID | 订单侧团期 ID |
|
||||
| payeeStaffId | body | Long | 是 | **须为本团主报账人的 staffId** | 取候选接口返回的 `id`;非主报账人返 589557 |
|
||||
| advanceType | body | String | 是 | 字典 `advance_type` | 借款类型,如 `TICKET` |
|
||||
| amount | body | BigDecimal | 是 | ≥ 0.01,且 ≤ 团期统一池可用余额 | 超额返 585004 |
|
||||
| purpose | body | String | 否 | ≤ 255 字 | 用途说明 |
|
||||
| voucherUrl | body | String | 否 | ≤ 512 字符 | 凭证 URL |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Long(String) | 预支单 ID |
|
||||
| orderId | Long | 团期级预支恒为 null |
|
||||
| payeeStaffId | Long(String) | 领款人 staffId(即主报账人) |
|
||||
| payeeName | String | 领款人姓名快照 |
|
||||
| payeeRole / payeeRoleText | String | 领款人角色及中文 |
|
||||
| advanceType | String | 借款类型 |
|
||||
| amount | BigDecimal | 预支金额 |
|
||||
| purpose | String | 用途说明 |
|
||||
| status / statusText | String | 创建后恒为 `SUBMITTED` / 「待审批」 |
|
||||
| createdByName | String | 发起人 |
|
||||
| createTime / submittedAt | String | 创建 / 提交时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2102967993891979266/advance
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"payeeStaffId": 1002,
|
||||
"advanceType": "TICKET",
|
||||
"amount": 100,
|
||||
"purpose": "门票备用金"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "2102968264214872066",
|
||||
"orderId": null,
|
||||
"payeeStaffId": "1002",
|
||||
"payeeName": "李雪梅",
|
||||
"payeeRole": "GUIDE",
|
||||
"payeeRoleText": "GUIDE",
|
||||
"advanceType": "TICKET",
|
||||
"amount": 100,
|
||||
"purpose": "门票备用金",
|
||||
"voucherUrl": null,
|
||||
"status": "SUBMITTED",
|
||||
"statusText": "待审批",
|
||||
"rejectReason": null,
|
||||
"createdByName": "admin",
|
||||
"createTime": "2026-09-24 11:48:00",
|
||||
"submittedAt": "2026-09-24 11:48:00",
|
||||
"approvedAt": null,
|
||||
"approvedBy": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口,无空数据形态。校验不通过时不落库,返回下方错误码之一。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| code | 触发条件 | 前端建议 |
|
||||
|---|---|---|
|
||||
| `589557` | **新增**。领款人在本团,但不是主报账人(次报账人 / 普通人员),或本团还没设主报账人 | 提示「请先在团期人员配置中设置主报账人」,引导去配人 |
|
||||
| `585007` | 领款人不在本团人员名单里 | 同前,刷新候选 |
|
||||
| `589541` | 团期处于核单中 / 已结算 / 已流团;**文案改为「当前团期状态不可发起预支(进入核单后关闭)」** | 隐藏或置灰「发起预支」 |
|
||||
| `585004` | 金额超过团期统一池可用余额 | 提示可用余额(财务面板 `advanceAvailable`) |
|
||||
| `589542` | **#8270 起不再返回**(四项配齐门已删除,码位保留不复用) | 前端若有针对它的分支可删除 |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589557,
|
||||
"message": "领款人须为本团主报账人,请先在团期人员配置中设置主报账人",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 允许状态:招募中、资源准备中、物料准备中、待出发、出行中、出行完毕;核单中 / 已结算 / 已流团拒绝。
|
||||
- 校验顺序:团期状态(589541)→ 领款人在本团(585007)→ 领款人是主报账人(589557)→ 借款类型 → 金额上限(585004)。
|
||||
- 上限规则不变:团期级与各子订单级预支共用一个团期尾款池。
|
||||
- 只影响团期级预支;订单级预支(`POST /v3/admin/order/{orderId}/advance`)的领款人规则不变。
|
||||
|
||||
### 2. 团期预支领款人候选 `GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates`
|
||||
|
||||
**VO**: `List<AdvancePayeeCandidateVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「发起预支」弹窗打开时拉取领款人下拉。本单起只会返回主报账人一条,前端可直接默认选中;返回空列表说明本团还没设主报账人。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| groupBatchId | path | Long | 是 | 运营团期 ID | 订单侧团期 ID,服务端内部换算产品侧排期 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Long(String) | 资源域 staffId,即发起预支的 `payeeStaffId` |
|
||||
| staffName | String | 姓名 |
|
||||
| staffRole / staffRoleText | String | 角色及中文 |
|
||||
| reporterRank | String | 恒为 `PRIMARY` |
|
||||
| isDefault | Boolean | 恒为 `true` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102967993891979266/advance/payee-candidates
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "1002",
|
||||
"staffName": "李雪梅",
|
||||
"staffRole": "GUIDE",
|
||||
"staffRoleText": "GUIDE",
|
||||
"reporterRank": "PRIMARY",
|
||||
"isDefault": true
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本团未配人员,或配了人但没设主报账人,返回空列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
判权与团期存在性校验未改动。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 条数只会是 0 或 1;不再返回次报账人与普通人员,也不再排序。
|
||||
- 字段结构不变,前端按字段取值的代码不用改。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 发起预支的 `payeeStaffId` 一律取候选接口返回的那一条,不要从团期人员列表里自己挑。
|
||||
- 候选为空时不要让用户提交(必然 589557),提示去团期人员配置里设置主报账人。
|
||||
- 「发起预支」按钮的可用状态按团期状态判断:进入核单(`REVIEWING`)之前都可用,不再要求四项资源配齐。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 零表结构变更、零迁移脚本。
|
||||
- 发起预支成功时照旧向 `order_advance` 写一行(`scope=GROUP_BATCH`、`order_id=NULL`、`payee_ref_type=BATCH_STAFF`);任何校验失败零写入(TEST 实测前后计数一致)。
|
||||
- 主报账人读自团期人员配置 `order_batch_staff.reporter_rank`,只读不写。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 主报账人在确认后锁定;取消成团(资源准备中 → 招募中)会回收导摄配置,主报账人可能随之被清掉,此时候选为空、发起预支返 589557,需重新配置。
|
||||
- 已发起的预支保存领款人快照,事后主报账人变更不影响已有单据。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | #8270 之前 | #8270(09-23) | 本单(09-24) |
|
||||
|---|---|---|---|
|
||||
| 可发起状态 | 物料准备中 ~ 出行完毕 | 资源准备中 ~ 出行完毕 | **招募中 ~ 出行完毕** |
|
||||
| 四项配齐门 | 要求,否则 589542 | 删除,589542 不再返回 | 同 #8270 |
|
||||
| 领款人 | 本团任一已配人员 | 同左 | **仅本团主报账人,否则 589557** |
|
||||
| 候选接口 | 本团全部人员,主报账人排首 | 同左 | **仅主报账人一条 / 空列表** |
|
||||
| 589541 文案 | 列出允许状态区间 | 同左 | 「当前团期状态不可发起预支(进入核单后关闭)」 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- 前端:候选下拉要处理 0 条;「发起预支」按钮若仍按旧规则(四项配齐 / 物料准备中起)置灰,需放开到核单前;新增 589557 文案。不改的话,功能按旧规则被前端挡住,但后端不会出错。
|
||||
- 已有数据:不回填、不改存量预支单。
|
||||
- 订单级预支、审批中心、结算核单均不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 订单级预支 6 个端点(`/v3/admin/order/{orderId}/advance*`、审批中心列表、通过 / 驳回)。
|
||||
- 团期财务总览 `GET /v3/admin/order/group-batch/{groupBatchId}/finance`、团期预支记录 `GET .../advances`。
|
||||
- 团期人员配置与报账人等级接口。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署 dev-v3 @ `980429ac2`(含 PR #8323)到 TEST 后,2026-09-24 11:47~11:49 经真实网关 `https://api.test.1814.love` 实测(自造招募中团期 `2102967993891979266`,四项资源全未配,主报账人 1002、另配摄影 1003):
|
||||
|
||||
| 用例 | 期望 | 实测 |
|
||||
|---|---|---|
|
||||
| 构建身份:样本团 `2100509904627191810` 候选接口 | 部署前 2 人 → 部署后仅主报账人 | 部署前 `[1002, 1003]`,部署后 6 次均 `[1002]` |
|
||||
| 候选接口(有主报账人) | 仅 1002,`isDefault=true` | 通过 |
|
||||
| 招募中 + 主报账人 1002 提交 100 | 200,`SUBMITTED`,落库 1 行 | 通过,`order_advance` 0→1 |
|
||||
| 领款人 1003(普通人员) | 589557,零写入 | 通过 |
|
||||
| 领款人 1003(设为次报账人后) | 589557,零写入 | 通过 |
|
||||
| 撤掉主报账人后候选 / 提交 | 候选 `[]`;提交 589557 | 通过 |
|
||||
| 领款人 1005(不在本团) | 585007 | 通过 |
|
||||
| 招募中提交 6000(池 5400) | 585004 | 通过 |
|
||||
| 核单中团期 `2100891433836675073` | 589541(新文案) | 通过 |
|
||||
| 已流团团期 `2102949787102076929` | 589541 | 通过 |
|
||||
| 不带 token | 401 | 通过 |
|
||||
|
||||
造数已清理:预支单驳回、订单取消、产品侧班期取消。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8322、PR #8323;#8270 / PR #8272
|
||||
- 团期预支复用订单预支:`changelogs-v2/2026-09/06_7154_团期财务总览与预支复用订单预支-新增接口-管理后台.md`
|
||||
- 报账人等级:`changelogs-v2/2026-09/23_8231_配导游配摄影物资放开到出行前四态-修改接口-管理后台.md`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg
|
||||
- 团期主报账人功能:wx
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8329"
|
||||
title: "团期配车确认重复调用不再返 602005:需求版本校验下沉到幂等分支之后,回写推进版本不再让重复确认变成错误"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "a8e6f9d5656366efa324eb9b8c9c46d4fc1fab57"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "PR #8332 squash 合并 dev-v3(8675e1ff0)。部署:hl-fleet-service dev-v3 @ 2b4656424(2026-09-24 14:03 滚动部署完成;同批 order-v3 亦为 2b4656424)。测试服网关从零实测(自造团期 groupBatchId=2103003304906936322,正式需求 2103003334258675714 v2,3 条配车行):① reconfigure(requirementVersion=2)→ 200;② confirm 第一次(requirementVersion=2)→ 200,confirmedCount=3、alreadyConfirmedCount=0、requirementAdvanceIntent=CONFIRMED_TO_DISPATCHED,回写把正式需求推到 status=DONE、version=3;③ 用提交时的旧版本再 confirm 一次(requirementVersion=2)→ http 200、code=200、message=成功,confirmedCount=0、alreadyConfirmedCount=3。改前同一形态返 602005「用车需求已更新, 请刷新后重新配车(提交 …/v2, 当前 …/v3)」。定向单测 81 绿(GroupDispatchConfirmTest 23 / GroupDispatchServiceTest 58),fleet spotless:check 901 files clean。闸门按「基线需求状态已是 DISPATCHED/DONE(即本端点回写已落地)」放行,状态停在 CONFIRMED/PENDING_RECONFIRM 时仍 fail-closed 抛 602005。 | 2026-09-24 mmg 交付:确认抽屉幂等档(confirmedCount=0 且 alreadyConfirmedCount>0)成功文案改「本团配车此前已全部确认(N 行),无需重复操作」,不渲染成定稿/失败;602005 仍透 message 按码处置;group-dispatch.js JSDoc 补幂等下沉语义;drawer spec 7 例全绿(幂等用例改断言)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet 团期配车: 确认重复调用不再返 602005(版本校验下沉)
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service(dispatch 域)
|
||||
> **PR**: https://git.1814.love/wx/HL/pulls/8332
|
||||
> **Issue**: #8329
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 车务「团期配车 → 确认配车」按钮的重复点击 / 刷新后重试
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- **重复确认不再返 602005**:整团配车确认成功后,本次确认会**推进正式用车需求版本**(回写);此前这个自己造成的版本前进会让「再点一次确认」被判成「需求已更新」,返回 602005 要求刷新重配——而刷新后拿到的还是同一个已完成的团。现在这一档返回 **200**,并以 `confirmedCount=0` / `alreadyConfirmedCount=N` 表达「本来就是已确认状态」。
|
||||
- **602005 仍然存在,且判据更精确**:只有当「本次端点的回写**尚未落地**」而版本/状态又对不上时才抛(fail-closed),即真正的「业务侧改了需求,你拿的是旧版本」。
|
||||
- **602007 的触发面变化**:本团一行配车行都没有(`assignedCount=0` 且 `alreadyConfirmedCount=0`)时抛 602007「本团没有可确认的配车行」,**不再**被版本校验抢先报成 602005。
|
||||
- **602005 报文的两个占位符统一成 `id=…/v…` 形态**:前端**只应按错误码 602005 处置,不要解析 {0} / {1}**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
确认配车是定稿动作,它认下的是「这一版需求对应的这套车」,因此入参带了 `requirementId` + `requirementVersion` 作为需求身份。问题是这个端点自己有副作用:确认成功会把正式需求从 `CONFIRMED` 回写成 `DISPATCHED`,版本因此前进。改前版本校验排在幂等分支**之前**、且在事务外先跑,于是「重复确认」这条正常操作被自己造成的版本前进判成了错误。实测:`POST .../confirm {requirementId:2102982327141556225, requirementVersion:2}` → `code=602005`「用车需求已更新, 请刷新后重新配车(提交 2102982327141556225/v2, 当前 2102982327141556225/v3)」,而同刻回读需求已是 `v3`——前进正是首次确认自己的回写。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 确认整团配车 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` | 行为变更(校验时机 + 报文占位符口径) | 重复确认返 200;602005 只在「本端点回写未落地」时抛 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 确认整团配车 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm`
|
||||
|
||||
**VO**: `GroupDispatchConfirmReqVO → GroupDispatchConfirmRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务在团期配车页点「确认配车」,把本团已排的车一次定稿。**没有防重提交时间窗**:连点多少次都是同一个形态,不会出现「请稍后重试」。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
| requirementId | Body | Long | ✅ | 须等于本团当前正式需求 | 不等于抛 602005(不受本次改动放宽) |
|
||||
| requirementVersion | Body | Integer | ✅ | 本次确认所依据的版本 | **语义细化(本单)**:版本不一致时——本团仍有「已派车」行待确认,或基线需求状态还停在 CONFIRMED / PENDING_RECONFIRM ⇒ 602005;基线需求状态已是 DISPATCHED / DONE(本端点回写已落地)⇒ 不抛,按 confirmedCount=0 返 200 |
|
||||
| remark | Body | String | ❌ | ≤200 | 仅日志留痕,**不写进配车行**(配车行 remark 参与 planDigest,刷进去会让下次重配误判「计划有变更」) |
|
||||
|
||||
#### 出参 `Result<GroupDispatchConfirmRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| confirmedCount | Integer | 本次真正由 ASSIGNED 转 CONFIRMED 的行数;重复确认时为 0 |
|
||||
| alreadyConfirmedCount | Integer | 本次之前就已是 CONFIRMED 的行数 |
|
||||
| requirementId / requirementVersion | String / Integer | 本次提交的需求身份(照原样回显) |
|
||||
| planVersion | Integer | 配车计划版本 |
|
||||
| requirementAdvanceIntent | String | 回写意图,如 `CONFIRMED_TO_DISPATCHED` |
|
||||
| coverage | Object | 按组覆盖读数(groups / missingGroupCodes / satisfied) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"requirementId": "2103003334258675714",
|
||||
"requirementVersion": 2,
|
||||
"remark": "与地接确认车辆无误"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2103003304906936322",
|
||||
"confirmedCount": 0,
|
||||
"alreadyConfirmedCount": 3,
|
||||
"requirementId": "2103003334258675714",
|
||||
"requirementVersion": 2,
|
||||
"planVersion": 2,
|
||||
"requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED",
|
||||
"coverage": { "satisfied": true, "missingGroupCodes": [] }
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 本团没有任何配车行时**不是**空成功,而是 602007(见「错误响应」)。
|
||||
- 本团有配车行但已全部 CONFIRMED:200 + `confirmedCount=0`、`alreadyConfirmedCount=N`。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "confirmedCount": 0, "alreadyConfirmedCount": 3 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602005,
|
||||
"message": "用车需求已更新, 请刷新后重新配车(提交 id=2103003334258675714/v2, 当前 id=2103003334258675714/v4)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602007,
|
||||
"message": "本团没有可确认的配车行, 请先提交配车计划",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **真实的判定顺序(改后)**:事务外只做需求 **ID** 校验(602005 的一种形态);进事务、取团锁、锁内重读配车行后依次判 —— 行数都为 0 ⇒ 602007;**本端点回写是否已落地**(基线需求状态 ∈ DISPATCHED / DONE)⇒ 落地则跳过版本/状态校验(幂等重放),未落地则照旧 fail-closed 抛 602005 / 602006;随后做按组覆盖校验(602008),再 CAS 确认。
|
||||
- **跳过的条件不是「没有待确认行」,而是「本端点的回写已经落地」**:状态停在 CONFIRMED / PENDING_RECONFIRM 而版本不等 ⇒ 推进版本的不是我们,是业务侧改了需求,必须照旧 602005。**不要按 assignedCount==0 一刀切。**
|
||||
- **需求被整份换过**(`requirementId` 都变了)时拿不到「提交版本」,报文只有 ID 那一半;两支统一拼 `id=…` 前缀。
|
||||
- 重配端点(`reconfigure`)**未变**:仍整份调用身份校验。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
- **前端按码处置,不解析报文**:602005 = 需求被业务侧改过,刷新重配;200 且 `confirmedCount=0` = 本来就是已确认,无需任何动作。
|
||||
- **不要把「重复确认」当成需要防抖的操作**:本端点没有防重提交时间窗,按钮可以做防抖,但服务端保证重复调用不报错。
|
||||
- 确认成功后若要改车,走重配端点(`reconfigure`)而不是再点确认。
|
||||
|
||||
### ✅ 正确 / ❌ 错误处置对照
|
||||
|
||||
```text
|
||||
✅ 200 + confirmedCount=0 → 已是确认态,直接展示「已确认」,不要提示失败
|
||||
✅ 602005 → 提示「需求已更新,请刷新后重新配车」
|
||||
❌ 把 200 + confirmedCount=0 渲染成错误或重复提交警告
|
||||
❌ 发现 602005 后原地重试同一个 requirementVersion(版本不会自己回来)
|
||||
```
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 重配(`reconfigure`)成功后配车行回到 ASSIGNED/待确认,需要再点一次确认;本单未改这条链路。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无表结构变更、无 Flyway。
|
||||
- 确认成功的写入是「把本团存活配车行由 ASSIGNED 置 CONFIRMED」+「事务内意图记录回写需求状态」;重复确认时 CAS 命中 0 行,**零写入**。
|
||||
- 602005 / 602006 / 602007 / 602008 都在写库之前抛出,可回读需求版本与配车行状态确认未变。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 并发下的保护未变:确认前取团级变更锁并在锁内重读配车行,命中行数与 assignedCount 不符即判并发。
|
||||
- 受控重开窗口(602012 / 602013)语义未变。
|
||||
- 需求被整份换过仍抛 602005,不受本次放宽影响。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
| 码 | 触发(改后) | 报文 |
|
||||
|---|---|---|
|
||||
| 602005 | requirementId 不等于基线;或本端点回写未落地(需求状态非 DISPATCHED/DONE)而 requirementVersion 不等于基线当前版本 | 用车需求已更新, 请刷新后重新配车(提交 {0}, 当前 {1}),两个占位符均为 id=需求ID/v版本 |
|
||||
| 602006(不变) | 需求状态不允许配车(回写未落地时的状态判据) | 正式用车需求当前状态不允许配车: {0} |
|
||||
| 602007(触发面变更) | 本团 assignedCount=0 且 alreadyConfirmedCount=0 | 本团没有可确认的配车行, 请先提交配车计划 |
|
||||
| 602008(不变) | 库里现存计划仍不满足按组覆盖 | 配车尚未覆盖完整, 不能确认: {0} |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 请求 / 响应字段 | — | 无新增、无删除,结构不变 |
|
||||
| 602005 的 {0} | 「换版」支不带前缀词 | 两支统一为 id=…/v… |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 首次确认成功 | 200,confirmedCount=N | 200,confirmedCount=N(不变) |
|
||||
| 需求版本已被本端点回写推进后再确认(旧版本) | **602005**(要求刷新重配) | **200**,confirmedCount=0、alreadyConfirmedCount=N |
|
||||
| 业务侧改了需求、本团仍有待确认行 | 602005 | 602005(不变,fail-closed) |
|
||||
| 业务侧改了需求、本团早已确认完(回写已落地) | 602005 | 200(幂等重放) |
|
||||
| 本团一行配车行都没有 | 可能先被版本校验报成 602005 | 602007 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:不破坏——只把「自己造成的版本前进」这一档从错误放宽成幂等成功;报文占位符口径变化不影响按码处置的前端。
|
||||
- **前端是否必须同步上线**:不必须。建议把 confirmedCount=0 渲染成「已确认」而不是失败。
|
||||
- **存量数据影响**:无迁移、无重算。
|
||||
- **上线风险**:放宽的闸门以「基线需求状态已是 DISPATCHED / DONE」为唯一理由,状态未落地时判据与改前完全一致,因此不会掩盖「业务侧改需求」这一真实冲突。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 的版本/状态校验时机与 602005 报文占位符形态。
|
||||
- **零影响**:
|
||||
- 重配端点 `POST .../reconfigure`(仍整份校验身份)。
|
||||
- 602006 / 602008 / 602012 / 602013 的判据与报文。
|
||||
- 按组覆盖校验、并发锁、受控重开窗口。
|
||||
- order-v3 侧对回写意图的幂等消费口径。
|
||||
- readiness / overview / 配车行数据结构、数据库、网关路由。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署读数**:`hl-fleet-service` dev-v3 @ `2b4656424`,2026-09-24 14:03 滚动部署完成。
|
||||
|
||||
**网关真实请求与响应**(`https://api.test.1814.love`,自造团期 `groupBatchId=2103003304906936322`,正式需求 `2103003334258675714`,3 条配车行):
|
||||
|
||||
```text
|
||||
① POST /admin/fleet/group-dispatch/batches/2103003304906936322/reconfigure
|
||||
{requirementId:2103003334258675714, requirementVersion:2, demands:[3 天 × BUS × 1 辆]}
|
||||
→ http 200, code=200;addedCount=3、aliveCount=3 ✓
|
||||
|
||||
② POST .../confirm {requirementId:2103003334258675714, requirementVersion:2}
|
||||
→ http 200, code=200, confirmedCount=3, alreadyConfirmedCount=0,
|
||||
requirementAdvanceIntent=CONFIRMED_TO_DISPATCHED ✓
|
||||
回读 GET /v3/admin/order/group-batch/2103003304906936322/vehicle-requirement
|
||||
→ status=DONE, version=3(版本被本次回写推进)✓
|
||||
|
||||
③ POST .../confirm {requirementId:2103003334258675714, requirementVersion:2} ← 用提交时的旧版本重复确认
|
||||
→ http 200, code=200, message=成功, confirmedCount=0, alreadyConfirmedCount=3 ✓(改前此处为 602005)
|
||||
```
|
||||
|
||||
**定向测试逐类读数**(`mvn -o -pl hl-fleet-service -am test -Dtest='GroupDispatchConfirmTest,GroupDispatchServiceTest' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,81 绿):
|
||||
|
||||
| 测试类 | Tests run |
|
||||
|---|---|
|
||||
| GroupDispatchServiceTest | 58 |
|
||||
| GroupDispatchConfirmTest | 23 |
|
||||
|
||||
fleet `spotless:check`:901 files clean、0 needs changes。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 确认配车端点与 `GroupDispatchConfirmReqVO` 原始设计:#7442
|
||||
- 受控重开窗口(602012 / 602013):#8308 系列 → `changelogs-v2/2026-09/`
|
||||
- 团期配车就绪检查:#8294 → `changelogs-v2/2026-09/24_8294_团期配车就绪检查座位不足黄牌扣司机座,新增-passengerSeatTotal-修改接口-管理后台.md`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- Issue: https://git.1814.love/wx/HL/issues/8329
|
||||
- 后端 PR: https://git.1814.love/wx/HL/pulls/8332
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: wx
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8330"
|
||||
title: "团期整团确认预检新增车侧缺失原因 MEMBER_GROUP_MISMATCH(809125):户级报的车型与覆盖它的乘车分组车型不符时拒绝放行"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "8785efa8c1ae711648be2016781a1fb63dc6a5c5"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "PR #8334 squash 合并 dev-v3(39d21cd4f)。部署:hl-order-service-v3 dev-v3 @ 2b4656424(2026-09-24 14:02 滚动部署两实例)。测试服网关实测(groupBatchId=2102981652823302146,生效正式需求 CONFIRMED v4:BUS=[丙]、MPV=[甲,乙];甲已把户级 TRAVEL 需求改成 bus 并放行):GET requirement/confirm-check → ready=false,vehicleMissing 新增一条 reason=MEMBER_GROUP_MISMATCH、orderId=甲、detail=该户报的车型为 bus,但覆盖它的乘车分组(MPV)车型为 mpv,没有车型相符的分组,请重新汇总后保存正式用车需求;同刻 POST requirement/confirm 在原 809109 上先被拒(该团期另有未覆盖户)。改前同一位置 vehicleMissing=[]、ready=true。定向单测 141 绿(CheckVehicleTest 20 / ConfirmVehicleTest 18 / GroupBatchRequirementServiceTest 73 / GroupVehicleRequirementConfirmCheckTest 30)。判据保守 fail-open:户级车型集合为空、分组车型归一失败、无活跃正式需求一律跳过,历史自由文本脏值不会误报。 | 2026-09-24 mmg 交付:实证 RequirementTab vehicleMissing 链路开箱即用(detail 直显+ready 置灰+orderNo 点击定位)零 UI 改动;orderV2GroupBatch.js JSDoc 补 reason 口径;spec 31 例全绿(新增 1 例回归锁)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3 团期需求: 新增 MEMBER_GROUP_MISMATCH(809125)车型-分组一致性预检
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(团期需求域,groupbatch 包)
|
||||
> **PR**: https://git.1814.love/wx/HL/pulls/8334
|
||||
> **Issue**: #8330
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的整团确认预检与整团确认
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- **新增车侧缺失原因 `MEMBER_GROUP_MISMATCH` 与错误码 809125**:户级 TRAVEL 需求改成另一种车型并放行后,若该户仍落在**车型不符**的分组里,整团确认预检会点名该户,`ready=false`;整团确认也会被拒。
|
||||
- **这是「补盲区」而不是「加新规则」**:既有的 809109 只问「该户的每一天有没有落在**某个**分组里」,不问「那个分组是不是这户要的车型」。户改了车型却仍在原分组 `memberOrderIds` 里时,809109 恒通过,一份与户级现状不符的正式需求会被放给车务。
|
||||
- **判据保守、fail-open**:只有当「该户报的车型集合」与「覆盖它的分组车型集合」**交集为空**时才报;任一集合为空(没报车型 / 不在任何分组里 / 分组车型归一不出来)一律跳过。历史脏值(自由文本车型、车型列未填的存量分组)不会被误报成不一致。代价是「说不清」的数据继续不报——漏报只是少一道提醒,误报会卡死整团确认。
|
||||
- **不自动改数据、不新增状态**:不改 `GroupVehicleCoverage`、不改车辆需求状态机、不回退已配车辆、不动 fleet 侧 `readiness`。处置是把不一致摆给团期管理员,由管理员「重新汇总并保存正式用车需求」收敛。
|
||||
- **前端必须展示新 reason**:`vehicleMissing[]` 目前只有 `OrderNo` 与 `orderId`,页面已把 `orderNo` 拼在 `detail` 之前,报文里不再带雪花 id;新 reason 的处置动作已写死在 `detail` 尾句。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
团期需求是「户级提交 → 团期管理员审核放行 → 团级汇总成正式需求 → 车务按正式需求配车」四段结构。户级需求在配置完成后仍会被改(客人换车型、加人加车、临时改行程),这是常态。房务侧对这条路径已有完整回退(`BATCH_ROOM_PLAN_BASELINE_RECONFIRMED` + `hotelReady` 回落 + 逐日 mismatch + 分房 stale),车务侧此前缺这一半:实测把某户由 `mpv 7 座` 改成 `bus 16 座` 并放行后,生效版本仍写着该户在 MPV 组,而 `aggregate-draft` 已把它算进 BUS 组,两份数字互相矛盾却全程零提示,`readiness` 仍 `ready=true`。本单落**方案②**(零 DDL、只做可见性),方案①(新增「过时」标记 + fleet 硬拦 + 配车完成回退)改动面大,留待产品决策。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 整团确认预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 响应新增枚举值 | `vehicleMissing[].reason` 新增 `MEMBER_GROUP_MISMATCH`,对应 809125 |
|
||||
| 2 | 整团确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 行为变更(同一判据) | 与预检同源,存在该缺失时整团确认被拒(不再静默通过) |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 整团确认预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
|
||||
|
||||
**VO**: `GroupBatchRequirementCheckRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求」Tab 打开或点「整团确认」前的只读预检。权限 `group-batch:demand:confirm`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键(不是产品班期 ID) |
|
||||
|
||||
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ready | Boolean | 是否可整体确认;本次起在「车型-分组不一致」时为 false |
|
||||
| vehicleMissing[] | Array | 车侧缺失项;本次新增 `MEMBER_GROUP_MISMATCH` |
|
||||
| vehicleMissing[].reason | String | 原因码,取值见「六.5」 |
|
||||
| vehicleMissing[].orderId / orderNo | String / String | 涉及的子订单(雪花 ID 按字符串处理) |
|
||||
| vehicleMissing[].groupCode / tripDate | String / LocalDate | 本原因这两项恒为 null(分歧在「组选错了」,不在某一天) |
|
||||
| vehicleMissing[].detail | String | 人话描述,与整团确认抛出的错误报文逐字相同,可直接展示 |
|
||||
| groupVehicleRequirementId / Status / Version | String / String / Integer | 当前生效正式需求的身份,`detail` 里照旧不出现 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102981652823302146/requirement/confirm-check
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102981652823302146",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusName": "资源准备中",
|
||||
"ready": false,
|
||||
"missing": [],
|
||||
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
|
||||
"vehicleWaived": false,
|
||||
"vehicleMissing": [
|
||||
{
|
||||
"reason": "MEMBER_GROUP_MISMATCH",
|
||||
"groupCode": null,
|
||||
"tripDate": null,
|
||||
"orderId": "2102981652680695809",
|
||||
"orderNo": "HL20260924124112051",
|
||||
"detail": "该户报的车型为 bus,但覆盖它的乘车分组(MPV)车型为 mpv,没有车型相符的分组,请重新汇总后保存正式用车需求"
|
||||
}
|
||||
],
|
||||
"vehicleExemptHouseholds": [],
|
||||
"groupVehicleRequirementId": "2102982327141556225",
|
||||
"groupVehicleRequirementStatus": "CONFIRMED",
|
||||
"groupVehicleRequirementVersion": 4
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 一切正常时 `vehicleMissing: []`、`ready: true`,接口仍 200。
|
||||
- 该户没报车型、不在任何分组里、分组车型归一不出来(自由文本 / 车型列未填)时**跳过本判据**,不报 809125(fail-open)。
|
||||
- 团期还没有生效中的正式需求时不报本判据。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "ready": true, "vehicleMissing": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本端点只读,业务失败以 `result` 包裹在 HTTP 200 内;本单不新增抛错分支。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "ready": false, "vehicleMissing": [{ "reason": "MEMBER_GROUP_MISMATCH" }] }, "success": true }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判据是「该户 `fleet[].vehicleType` 归一后的集合」与「逐日覆盖它的那些分组的 `vehicleType` 集合」**交集为空**;只要有一个分组车型相符就不报。
|
||||
- 与 809109 互补且不重叠:809109 管「不在任何分组里」,809125 管「在分组里但组选错了」。
|
||||
- 一个户最多报一条(不分天、不分组)。
|
||||
- 报文不含雪花 id,由页面用清单行自带的 `orderNo` 定位;`detail` 尾句已写死处置动作。
|
||||
|
||||
---
|
||||
|
||||
### 2. 整团确认 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm`
|
||||
|
||||
**VO**: `GroupBatchRequirementCheckRespVO`(确认口无请求体,与预检同源判据)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期管理员点「整团确认」,把各户需求汇总成正式需求并放行给车务。权限同上。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
|
||||
#### 出参 `Result<...>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementConfirmed | Boolean | 本次是否完成整体确认;被拒时不返回该结构 |
|
||||
| vehicleDispatchedOrderIds | Array<String> | 本次被放行的户 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2102981652823302146/requirement/confirm
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "requirementConfirmed": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
确认链路与预检读同一份快照;预检不报 809125 时确认也不会报。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "requirementConfirmed": false }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809125,
|
||||
"message": "该户报的车型为 bus,但覆盖它的乘车分组(MPV)车型为 mpv,没有车型相符的分组,请重新汇总后保存正式用车需求",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 确认按「第一条缺失」抛出,异常顺序为:团级六条 → 户级未提交(809122)→ 接送机未回填(809007)→ **车型不符(809125)**。排在 809122 之后是因为「重新汇总」本身会被未提交户的 809121 挡住,先催未提交的户才有意义。
|
||||
- 本原因不改变任何既有场景下「确认抛第一条」抛出的那一码。
|
||||
- 整团确认成功后正式需求会被重新汇总保存,管理员再次触发时本原因自然消失(状态收敛)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
- `reason` 是**稳定的机器可判值**,前端按它分支渲染;`detail` 只用于直接展示,**不要解析 `detail`**。
|
||||
- `vehicleMissing` 的顺序即处置顺序:先处理靠前的条目再刷新预检。
|
||||
- 页面在 `ready=false` 时不得放开「整团确认」按钮;即便放开,服务端也会以同一判据返回 809125。
|
||||
|
||||
### ✅ 正确 / ❌ 错误处置对照
|
||||
|
||||
```text
|
||||
✅ ready=false 且含 MEMBER_GROUP_MISMATCH → 走「自动汇总」→ 检查车型分组 → 保存正式用车需求 → 重新预检
|
||||
❌ ready=false 却直接点整团确认 → 809125(HTTP 200 业务失败)
|
||||
❌ 只改团级分组的 remark / 座位数就再保存 → 分组归属不变,本原因不会消失
|
||||
```
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 重新汇总并保存后正式需求进入 DRAFT 且 version +1,车务需按新版本重新配车确认(既有语义,未变)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无表结构变更、无 Flyway。
|
||||
- 本原因的判定发生在**写库之前**:整团确认抛 809125 时零写入(正式需求版本、状态、分组均不变,可回读确认)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 存量数据不批量重算:历史自由文本车型 / 车型列未填的分组继续不报(fail-open)。
|
||||
- 判据只在团级读面(`confirm-check` / `confirm`)生效;车务 `readiness` 与配车行不动。
|
||||
- 车主车型与乘车分组都是多值时取集合,任一相交即视为一致。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
| reason | 错误码 | 触发 | 报文 |
|
||||
|---|---|---|---|
|
||||
| `MEMBER_GROUP_MISMATCH` | 809125 | 该户报的车型与覆盖它的分组车型无交集 | 该户报的车型为 {0},但覆盖它的乘车分组({1})车型为 {2},没有车型相符的分组,请重新汇总后保存正式用车需求 |
|
||||
| `ORDER_DAY_UNCOVERED`(不变) | 809109 | 该户某天不在任何分组里 | 该子订单的 {0} 没有被任何乘车分组覆盖 |
|
||||
| `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(不变) | 809122 | 户级根本没提交 | 见既有条目 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `confirm-check` 请求/响应字段 | — | 无新增、无删除;`vehicleMissing[].reason` 多一个取值 |
|
||||
| `vehicleMissing` 条数 | — | 最多每户多一条 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 户改车型并放行、该户仍在旧车型分组里 | 预检 `ready=true`、`vehicleMissing=[]`;整团确认静默通过 | 预检 `ready=false` 并点名该户;整团确认 809125 拒绝 |
|
||||
| 户改车型但新车型有相符分组 | 通过 | 通过(不变) |
|
||||
| 户没报车型 / 分组车型归一不出来 | 通过 | 通过(fail-open 跳过) |
|
||||
| 团级分组与户级完全一致 | 通过 | 通过(不变) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:响应结构不变,但**同一场景下 `ready` 可能由 true 变 false、`confirm` 可能由 200 变 809125**。凡「户级车型与覆盖分组车型无交集」的团期都会被拒,直到管理员重新汇总保存。
|
||||
- **前端是否必须同步上线**:**建议同批**。不同步时新 reason 会以未知值出现在清单里(`detail` 已是完整人话,仍可展示),但页面若只看 `ready` 就足以正确禁用确认按钮。
|
||||
- **存量数据影响**:不批量重算、不迁移;只有下一次预检 / 确认才会暴露。
|
||||
- **上线风险**:判据 fail-open,历史脏值不会误报;但车型与分组确实不一致的团期会在确认时被拦,需要管理员走一次「重新汇总并保存」。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`GET .../requirement/confirm-check` 的 `ready` / `vehicleMissing`;`POST .../requirement/confirm` 的拒绝条件。
|
||||
- **零影响**:
|
||||
- 车务 `GET /admin/fleet/group-dispatch/batches/{gb}/readiness`(方案①范围,本单不做)。
|
||||
- 配车行、已配车辆、「已配车辆不动」语义。
|
||||
- 团级六条既有校验、809116 / 809118 / 809119 / 809120 / 809121 / 809122 / 809007 等其余判据与报文。
|
||||
- `GET .../vehicle-requirement`、`aggregate-draft`、`vehicle-households`、`requirement-summary`。
|
||||
- 子订单级用车需求与 `GroupVehicleCoverage`。
|
||||
- 数据库、网关路由、fleet 服务代码。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署读数**:`hl-order-service-v3` dev-v3 @ `2b4656424`,2026-09-24 14:02 滚动部署两实例(8086/8186 均 UP)。
|
||||
|
||||
**网关真实请求与响应**(`https://api.test.1814.love`,`groupBatchId=2102981652823302146`,生效正式需求 CONFIRMED v4:BUS=[丙]、MPV=[甲,乙];甲 `2102981652680695809` 的户级 TRAVEL 需求已改成 `bus 16 座` 并放行):
|
||||
|
||||
```text
|
||||
① GET /v3/admin/order/group-batch/2102981652823302146/requirement/confirm-check
|
||||
→ http 200, code=200, ready=false
|
||||
vehicleMissing 含一条:
|
||||
reason=MEMBER_GROUP_MISMATCH, orderId=2102981652680695809, orderNo=HL20260924124112051
|
||||
detail=该户报的车型为 bus,但覆盖它的乘车分组(MPV)车型为 mpv,没有车型相符的分组,请重新汇总后保存正式用车需求 ✓
|
||||
|
||||
② 同刻 POST .../requirement/confirm → 被既有 809109 先拦(该团期另有一户不在任何分组里),
|
||||
说明 809125 已并入同一条缺失链且不改变「确认抛第一条」的既有语义 ✓
|
||||
|
||||
改前同位置读数(2026-09-24 13:03,同一团期同一数据):ready=true、vehicleMissing=[] ✓
|
||||
```
|
||||
|
||||
**定向测试逐类读数**(`mvn -o -pl hl-order-service-v3 -am test -Dtest='…' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,4 个类合计 141 绿):
|
||||
|
||||
| 测试类 | Tests run |
|
||||
|---|---|
|
||||
| GroupVehicleRequirementConfirmCheckTest | 30 |
|
||||
| GroupBatchRequirementServiceTest | 73 |
|
||||
| GroupBatchRequirementServiceCheckVehicleTest | 20 |
|
||||
| GroupBatchRequirementServiceConfirmVehicleTest | 18 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 户级未提交(809122)先例:#8249 → `changelogs-v2/2026-09/`
|
||||
- 逐日覆盖校验 809109:#8235 → `changelogs-v2/2026-09/`
|
||||
- 团级座位档位校验 809124:#8311 → `changelogs-v2/2026-09/24_8311_团期分组座位数改档位下拉与809124校验-修改接口-管理后台.md`
|
||||
- 房侧同源回退(对照实现):`BATCH_ROOM_PLAN_BASELINE_RECONFIRMED`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- Issue: https://git.1814.love/wx/HL/issues/8330
|
||||
- 后端 PR: https://git.1814.love/wx/HL/pulls/8334
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: wx
|
||||
@@ -0,0 +1,427 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8331"
|
||||
title: "接送机需求窗不跟随新增大交通方向:整团确认预检新增 TRANSFER_WINDOW_INCOMPLETE(809126),605062 报文改为点名版本、越窗日期与处置方"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c88fd79478246ec68d7ca86267d90a9839f04fee"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "PR #8335 squash 合并 dev-v3(2b4656424)。部署:hl-order-service-v3 + hl-fleet-service dev-v3 @ 2b4656424,2026-09-24 14:02—14:03 滚动部署(order-v3 两实例 8086/8186 均 UP,fleet 两实例滚动完成)。测试服网关实测(groupBatchId=2102981652823302146;甲 2102981652680695809 的接送机需求 2102989264394563586 窗内只有 2027-02-03,之后补录了 2027-02-05 返程大交通):① GET requirement/confirm-check → ready=false,vehicleMissing 新增 reason=TRANSFER_WINDOW_INCOMPLETE、tripDate=2027-02-05、detail=该户接送机需求窗仅含 2027-02-03,大交通送机 2027-02-05 未进窗,请定制师重新提交接送机需求后再派车(改前该位置 vehicleMissing=[]);② POST /admin/fleet/assignments 派 2027-02-05 送机车 → code=605062,报文已改为:派车日期 2027-02-05 越出当前接送机需求日期窗(版本 v1,窗内服务日 2027-02-03):请先调整或取消这些越窗槽位,或让定制师重新提交接送机需求换版后再派车(改前为「存在派车日期与当前用车需求不符的槽位,请先调整或取消后再继续」)。定向单测:order-v3 157 绿、fleet AssignmentServiceTest 565 绿,fleet spotless:check 901 files clean。只对「提交得了的户」报(与 809122 同一份豁免判据取交集),避免对没有提交入口的户把整团确认永久卡死。 | 2026-09-24 mmg 交付:confirm-check 侧实证开箱即用(detail 直显),JSDoc 补 809126 口径+spec 回归锁;useAssignFlow 605062 一码两业务注释订正+缺省回退文案中性化,新报文直接透;spec 32+34+47 例全绿(新增 2 例,2 例改中性断言)"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3 + fleet 接送机: 需求窗缺方向预检(809126)与 605062 可执行报文
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(团期需求域 + requirement 域)· hl-fleet-service(assignment 域)
|
||||
> **PR**: https://git.1814.love/wx/HL/pulls/8335
|
||||
> **Issue**: #8331
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的整团确认预检与整团确认;车务「派车」写入口的 605062 报文
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- **新增车侧缺失原因 `TRANSFER_WINDOW_INCOMPLETE` 与错误码 809126**:某户的大交通派生出了新的方向日期(典型:先报到达、后补返程),而该户接送机需求的 `service_dates` 里没有这一天时,整团确认预检点名该户并 `ready=false`。
|
||||
- **605062 报文重写(码值与判据不变)**:由「存在派车日期与当前用车需求不符的槽位,请先调整或取消后再继续」改为点名**哪一版需求、哪一天越窗、窗内有哪些服务日、谁该做什么**。**这是一个既有错误码的报文变更,前端/QA 的文案断言需要同步更新。**
|
||||
- **只对「提交得了的户」报**:候选户与 809122 那份豁免判据取交集(订单在定制中且团期阶段开放提交,或该户最新需求被打回)。提交不了的户此刻在系统里没有提交入口,报出来只会把整团确认**永久卡死**(户补不了、团也确认不了)。
|
||||
- **不自动改需求数据、不自动换版**:换版要重走提交链(服务日重新派生、状态回待审核、记录换版原因),由定制师发起才是这个事实的属主该做的事。方案①(新方向大交通落库即自动换版)与 `requirementVersion` 身份校验、已派车行 superseded 处置耦合,留待产品决策。
|
||||
- **成功路径仍要求定制师重新提交**:本次只保证「不一致在整团确认前被看见 + 车务拿到的 605062 可执行」。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
大交通天然是分次录入的(先定去程、回程后补)。接送机的服务日只在**提交那一刻**由大交通派生一次、随后冻结在需求行上;补录一个**此前完全不存在方向**的大交通与「把已有航班改签」是两件事——前者是新增缺口,不是对已冻结窗口的修改。此前的现场是:车务看板按大交通报出「有送机缺口」,车务去派送机车却被 605062 拦死,报文既不点名越窗日期也不指出处置方,车务在派车侧怎么调整都推不动。本单落**方案②**(零 DDL、只做可见性与报文)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 整团确认预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 响应新增枚举值 | `vehicleMissing[].reason` 新增 `TRANSFER_WINDOW_INCOMPLETE`,对应 809126 |
|
||||
| 2 | 整团确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 行为变更(同一判据) | 存在该缺失时整团确认被拒 |
|
||||
| 3 | 创建派车(单槽位占位) | POST | `/admin/fleet/assignments` | 报文变更(码值与判据不变) | 605062 报文改为点名版本、越窗日期、窗内服务日与处置方 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 整团确认预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
|
||||
|
||||
**VO**: `GroupBatchRequirementCheckRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求」Tab 打开或点「整团确认」前的只读预检。权限 `group-batch:demand:confirm`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键(不是产品班期 ID) |
|
||||
|
||||
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ready | Boolean | 是否可整体确认;本次起在「接送机需求窗缺方向」时为 false |
|
||||
| vehicleMissing[] | Array | 车侧缺失项;本次新增 `TRANSFER_WINDOW_INCOMPLETE` |
|
||||
| vehicleMissing[].reason | String | 原因码,取值见「六.5」 |
|
||||
| vehicleMissing[].tripDate | LocalDate | 本原因下为**首个越窗日期**(大交通派生出来、却不在需求窗内的那天) |
|
||||
| vehicleMissing[].orderId / orderNo | String / String | 涉及的子订单 |
|
||||
| vehicleMissing[].groupCode | String | 本原因恒为 null(接送机需求没有乘车分组维度) |
|
||||
| vehicleMissing[].detail | String | 人话描述,与整团确认抛出的报文逐字相同,已含「请定制师重新提交接送机需求后再派车」 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102981652823302146/requirement/confirm-check
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102981652823302146",
|
||||
"ready": false,
|
||||
"vehicleMissing": [
|
||||
{
|
||||
"reason": "TRANSFER_WINDOW_INCOMPLETE",
|
||||
"groupCode": null,
|
||||
"tripDate": "2027-02-05",
|
||||
"orderId": "2102981652680695809",
|
||||
"orderNo": "HL20260924124112051",
|
||||
"detail": "该户接送机需求窗仅含 2027-02-03,大交通送机 2027-02-05 未进窗,请定制师重新提交接送机需求后再派车"
|
||||
}
|
||||
],
|
||||
"groupVehicleRequirementId": "2102982327141556225",
|
||||
"groupVehicleRequirementStatus": "CONFIRMED",
|
||||
"groupVehicleRequirementVersion": 4
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 全部一致时 `vehicleMissing: []`、`ready: true`。
|
||||
- 该户**提交不了**(订单不在定制中,或团期阶段已关提交且最新需求未被打回)时**跳过**本判据,不报 809126。
|
||||
- 该户没有活跃接送机需求、或大交通没有派生任何方向日期时不报。
|
||||
- 接送机需求窗与派生日期完全一致时不报。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "ready": true, "vehicleMissing": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
本端点只读,业务结果包在 HTTP 200 内。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "ready": false, "vehicleMissing": [{ "reason": "TRANSFER_WINDOW_INCOMPLETE" }] }, "success": true }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判据 =「当前大交通按方向派生的日期集合」−「需求行冻结的 `service_dates`」非空;只取**首个**越窗日期进 `tripDate`,多条时 `detail` 里顿号分隔。
|
||||
- 判据取「在团户」而不是「放行集合」:接送机需求是户级自己报的,与团级 `needs_vehicle` 无关。
|
||||
- 与 809007(待放行接送机需求未回填服务日)互斥且不重叠:809007 管「一行服务日都没有」,809126 管「有窗但窗比大交通旧」。
|
||||
- **同方向改签不报**:判据落在「缺的方向/日期」上,不是「大交通变了」。
|
||||
|
||||
---
|
||||
|
||||
### 2. 整团确认 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm`
|
||||
|
||||
**VO**: `GroupBatchRequirementCheckRespVO`(确认口无请求体,与预检同源判据)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期管理员点「整团确认」,把各户需求汇总成正式需求并放行给车务。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
|
||||
#### 出参 `Result<...>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementConfirmed | Boolean | 本次是否完成整体确认;被拒时不返回该结构 |
|
||||
| vehicleDispatchedOrderIds | Array<String> | 本次被放行的户 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2102981652823302146/requirement/confirm
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "requirementConfirmed": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
与预检读同一份快照;预检不报 809126 时确认也不会报。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "requirementConfirmed": false }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809126,
|
||||
"message": "该户接送机需求窗仅含 2027-02-03,大交通送机 2027-02-05 未进窗,请定制师重新提交接送机需求后再派车",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 异常顺序:团级六条 → 户级未提交(809122)→ 接送机未回填(809007)→ **接送机窗不完整(809126)** → 车型不符(809125)。
|
||||
- 「车型不符(809125)」排在 809126 之后:前者的处置是管理员**重新汇总**,而户级还在改(定制师要重提接送机需求)时先汇总,改完还得再汇总一次。
|
||||
|
||||
---
|
||||
|
||||
### 3. 创建派车(单槽位占位) `POST /admin/fleet/assignments`
|
||||
|
||||
**VO**: `AssignmentCreateReqVO`(沿用既有请求体,本单未改结构)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务按看板缺口派车。本单只改**被拒时的报文**,不改编排、判据与码值。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | Long | ✅ | 在团户 | 子订单 |
|
||||
| requirementId | Body | Long | ✅ | 该户当前活跃需求 | 与 `requirementVersion` 共同构成需求身份 |
|
||||
| startDate / endDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本次派车的服务日段(逐日行) |
|
||||
| headcount | Body | Integer | ✅ | ≥1 | 用车人数 |
|
||||
| requestId | Body | String | ✅ | 幂等标识 | 重复提交同值即幂等 |
|
||||
|
||||
#### 出参 `Result<...>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| (结构未变) | — | 本单只改业务失败时的 605062 报文 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": "2102981652680695809",
|
||||
"requirementId": "2102989264394563586",
|
||||
"vehicleId": "2085284111341023234",
|
||||
"driverId": "2089691308707733506",
|
||||
"startDate": "2027-02-05",
|
||||
"endDate": "2027-02-05",
|
||||
"headcount": 2,
|
||||
"sendItinerarySms": false,
|
||||
"confirmCrossResident": true,
|
||||
"requestId": "GRE2E-win-0205"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "assignmentId": "2103003339824435202" }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 需求窗内派车:正常 200,报文未变。
|
||||
- 该户当天已有在途槽位:仍走既有 605003 / 605004 等码,本单未改。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "addedCount": 1 }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605062,
|
||||
"message": "派车日期 2027-02-05 越出当前接送机需求日期窗(版本 v1,窗内服务日 2027-02-03):请先调整或取消这些越窗槽位,或让定制师重新提交接送机需求换版后再派车",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **四个实参**:{0}=越窗派车日期(升序、顿号分隔)、{1}=需求类别的人话名(接送机需求 / 行程用车需求)、{2}=需求版本号、{3}=当前需求窗内的服务日。全部由抛点拼成**字符串**传入——数字与日期直接交给 `MessageFormat` 会被本地化或插入千分位(版本号会变成 1,234)。
|
||||
- **两个入口共用同一份报文**:「在途槽位行越窗」与「请求日期段越窗」现在从同一个构造点出来,不会出现同一次越窗、两个入口不同指引。
|
||||
- **判据与码值不变**:仍然只按日期窗拦截,车型 / 数量 / 人数不符一律不拦。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
- `reason` 是稳定的机器可判值;`detail` 直接展示,**不要解析**。
|
||||
- 605062 是「车务侧无解」的码:**不要**在页面引导车务反复调整槽位,正确下一步是让定制师重新提交接送机需求(换版)后再派车。新报文已把这句话写进 message。
|
||||
- 需求版本会随换版 +1,换版后车务必须按新版本重新配车确认(既有语义)。
|
||||
|
||||
### ✅ 正确 / ❌ 错误处置对照
|
||||
|
||||
```text
|
||||
✅ 预检 ready=false 含 TRANSFER_WINDOW_INCOMPLETE → 通知该户定制师重新提交接送机需求 → 团期管理员重新整团确认 → 车务再派车
|
||||
❌ 车务反复改派车日期:需求窗没变,改到哪一天都还是 605062
|
||||
❌ 把需求窗当成可以手工改的字段:需求服务日由大交通派生,只能通过重新提交换版
|
||||
```
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
- 定制师换版后,该户需求进入 PENDING_REVIEW,团期管理员需重新整团确认放行;届时 `requirementVersion` 变化,车务侧旧版本身份校验会失败(既有 602005 语义)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无表结构变更、无 Flyway。
|
||||
- 809126 在**写库之前**抛出:整团确认被拒时零写入(正式需求版本与状态不变,可回读确认)。
|
||||
- 605062 在**写库之前**抛出:没有任何派车行落库。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 判据按「当前大交通派生的日期 − 需求窗」求差,同方向改签(日期变了但方向仍齐)不报——这是刻意保留「冻结不覆盖」的既有定案。
|
||||
- 只对「提交得了的户」报;提交不了的户不报,避免整团确认被永久卡死。
|
||||
- 多条越窗日期时 `tripDate` 取首个,`detail` / 805062 报文列出全部。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
| reason / 错误码 | 触发 | 报文 |
|
||||
|---|---|---|
|
||||
| `TRANSFER_WINDOW_INCOMPLETE` / 809126 | 该户大交通派生出的日期不在活跃接送机需求窗内,且该户此刻提交得了 | 该户接送机需求窗仅含 {0},大交通{1} 未进窗,请定制师重新提交接送机需求后再派车 |
|
||||
| 605062(报文变更,码值不变) | 派车日期越出当前需求窗 | 派车日期 {0} 越出当前{1}日期窗(版本 v{2},窗内服务日 {3}):请先调整或取消这些越窗槽位,或让定制师重新提交{1}换版后再派车 |
|
||||
| `TRANSFER_SERVICE_DATES_NOT_BACKFILLED`(不变) / 809007 | 待放行接送机需求一行服务日都没有 | 既有报文 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `confirm-check` 请求/响应字段 | — | 无新增、无删除;`vehicleMissing[].reason` 多一个取值 |
|
||||
| 605062 `message` | 存在派车日期与当前用车需求不符的槽位,请先调整或取消后再继续 | 派车日期 {0} 越出当前{1}日期窗(版本 v{2},窗内服务日 {3}):请先调整或取消这些越窗槽位,或让定制师重新提交{1}换版后再派车 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 先提接送机需求、后补录新方向大交通 | 预检 `ready=true`;车务按看板缺口派车被 605062 拦死且报文无出路 | 预检 `ready=false` 点名该户;605062 报文点名版本、越窗日期与处置方 |
|
||||
| 同方向改签 | 不换版、需求窗不变 | 不变(判据落在「缺的方向/日期」) |
|
||||
| 提交不了的户窗不完整 | 无提示 | 仍无提示(刻意 fail-open,避免永久卡死) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**:`confirm-check` 响应结构不变但**同一场景 `ready` 可能由 true 变 false**;605062 **报文文本变更**,凡断言旧文案的自动化脚本需同步。
|
||||
- **前端是否必须同步上线**:**建议同批**。新 reason 未适配时仍可由 `ready=false` 正确禁用确认按钮;605062 报文是纯文案替换,页面直接透传即可。
|
||||
- **存量数据影响**:不批量重算、不改数据;只有下一次预检 / 确认 / 派车才会暴露。
|
||||
- **上线风险**:对「窗比大交通旧且该户提交得了」的团期会在整团确认时被拦,需要定制师换版一次;提交不了的户不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**:`GET .../requirement/confirm-check` 的 `ready` / `vehicleMissing`;`POST .../requirement/confirm` 的拒绝条件;605062 的报文文本。
|
||||
- **零影响**:
|
||||
- 605062 的**判据与码值**(仍只拦日期越窗;车型 / 数量 / 人数不拦)。
|
||||
- 派车 / 排车 / 看板的缺口计算与 `overview` 字段。
|
||||
- 需求数据本身:不自动换版、不写 `service_dates`。
|
||||
- 团级六条既有校验与 809116 / 809118 / 809119 / 809120 / 809121 / 809122 / 809007 等其余判据与报文。
|
||||
- 数据库、网关路由。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署读数**:`hl-order-service-v3` 与 `hl-fleet-service` 均 dev-v3 @ `2b4656424`,2026-09-24 14:02—14:03 滚动部署完成。
|
||||
|
||||
**网关真实请求与响应**(`https://api.test.1814.love`,`groupBatchId=2102981652823302146`;甲 `2102981652680695809` 的接送机需求 `2102989264394563586` 窗内只有 `2027-02-03`,之后补录了 `2027-02-05` 返程大交通):
|
||||
|
||||
```text
|
||||
① GET .../requirement/confirm-check
|
||||
→ http 200, code=200, ready=false
|
||||
vehicleMissing 含一条:
|
||||
reason=TRANSFER_WINDOW_INCOMPLETE, tripDate=2027-02-05,
|
||||
orderId=2102981652680695809, orderNo=HL20260924124112051
|
||||
detail=该户接送机需求窗仅含 2027-02-03,大交通送机 2027-02-05 未进窗,请定制师重新提交接送机需求后再派车 ✓
|
||||
|
||||
② POST /admin/fleet/assignments(派 2027-02-05 送机车)
|
||||
→ http 200, code=605062
|
||||
message=派车日期 2027-02-05 越出当前接送机需求日期窗(版本 v1,窗内服务日 2027-02-03):
|
||||
请先调整或取消这些越窗槽位,或让定制师重新提交接送机需求换版后再派车 ✓
|
||||
|
||||
改前读数(2026-09-24 13:1x,同一团期同一数据):
|
||||
confirm-check → ready=true、vehicleMissing=[]
|
||||
assignments → code=605062,message=存在派车日期与当前用车需求不符的槽位,请先调整或取消后再继续 ✓
|
||||
```
|
||||
|
||||
**定向测试逐类读数**:
|
||||
|
||||
| 模块 / 测试类 | Tests run |
|
||||
|---|---|
|
||||
| hl-order-service-v3(5 个类合计) | 157 |
|
||||
| ├ GroupBatchRequirementServiceTest | 73 |
|
||||
| ├ GroupVehicleRequirementConfirmCheckTest | 30 |
|
||||
| ├ GroupBatchRequirementServiceCheckVehicleTest | 27 |
|
||||
| ├ GroupBatchRequirementServiceConfirmVehicleTest | 19 |
|
||||
| └ TransferServiceDatesResolverTest | 8 |
|
||||
| hl-fleet-service / AssignmentServiceTest | 565 |
|
||||
|
||||
fleet `spotless:check`:901 files clean、0 needs changes。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 户级未提交(809122)先例:#8249 → `changelogs-v2/2026-09/`
|
||||
- 车型与分组不符(809125,同日并入):#8330 → `changelogs-v2/2026-09/24_8330_团期整团确认预检新增车型与分组不符原因MEMBER_GROUP_MISMATCH与809125-修改接口-管理后台.md`
|
||||
- 605062 越窗门禁来源:#5810 / #8235
|
||||
- 接送机需求提交与 `transfer/batch-confirm`:#8202 / #8152
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- Issue: https://git.1814.love/wx/HL/issues/8331
|
||||
- 后端 PR: https://git.1814.love/wx/HL/pulls/8335
|
||||
|
||||
### 联系人
|
||||
|
||||
- 后端: wx
|
||||
@@ -0,0 +1,818 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8339"
|
||||
title: "团期人工确认联动子订单确认行程:确认门加逐户预检(589556 逐户列订单号 + 未满足项);团期单在团期确认前单户确认行程返新码 581065;时间线新增 BATCH_SUB_ORDER_CONFIRM_FAILED;合同保险出具门补出行完毕"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "65ce203dc20e5b475d52ce6797ec1b069f52cfff"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "六节点定案(SRS §0.27.7):团期人工确认时,团期单子订单一并确认行程(定制中 → 待出行),团期单不再允许单户自行确认,合同保险只由逐户确认链路出具。POST /v3/admin/order/group-batch/{groupBatchId}/confirm 的确认门在四项 ready + 物资已确认之外加逐户预检(待支付 / 定制中的活跃户须已付订金、出行人齐全且有成人手机号、户级房车完成、合同模板已设、有主报账人),不满足仍返 589556,message 由「、」连接的团期级清单改为「;」分段:首段团期级未满足项(有才出现),其后每户一段「订单 {订单号}:{未满足项}」;确认成功后事务提交后系统异步逐户确认行程,合同保险由逐户链路出具(#8268 的确认后全团补发下线),单户失败不回滚团期确认,写团期时间线新事件 BATCH_SUB_ORDER_CONFIRM_FAILED(extra 带 orderId / orderNo / reason)。POST /v3/admin/order/{id}/confirm-itinerary 与 POST /v3/admin/order/{id}/transition(eventCode=CONFIRM)对团期单在团期确认前返新码 581065「团期单随团期确认,不可单独确认」,团期确认后仍在定制中的户放行作补确认,散客单不受影响。合同保险出具门可出具状态集补 TRIP_FINISHED(出行完毕):GET .../contracts 的 issuable 在出行完毕为 true,contracts/issue、contracts/reissue 在出行完毕不再返 589548。前端需:589556 按「;」分段逐户展示;团期单订单详情在团期确认前隐藏或置灰「确认行程」并处理 581065;时间线显示新事件(eventTypeName 后端已给)。 前端已交付并验证(hl-admin@65ce203d):订单详情底栏「确认订单」团期单+团期未确认置灰+NTooltip 说明(isGroupConfirmBlocked 共享判据,六态集与合同保险出具门同源,null fail-open/非空未知 fail-closed),openLockConfirmModal 同源防御闸拦旁路入口;团期详情 confirm 文案改逐户确认行程链路,589556 改 silentError+dialog 按「;」分段逐行展示(不按「、」切碎/不解析订单号),其余码直弹 message;StatusLogsPanel 新事件 eventTypeName 直显零改动;contract.js 出具门补 TRIP_FINISHED JSDoc。4 个定向 spec 58 例全绿,新增 #8339 用例 25 例。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期确认: 确认门加逐户预检、子订单随团期确认行程、团期单禁单户确认(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8346(主体)、#8349(TEST 验收缺陷修复:主报账人副本补齐、逐户确认失败留痕)
|
||||
> **Issue**: #8339
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期详情页「确认」按钮的 589556 提示、团期时间线、「合同保险」页签的可出具判断;订单详情页「确认行程」按钮(团期单)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **团期确认会连带确认子订单行程**。确认成功后,系统在后台逐户把「定制中」的户推到「待出行」,每户的合同与保险随之自动出具;前端不需要再逐户点「确认行程」。
|
||||
2. **589556 的 message 格式变了**:由「、」连接的一串,变成「;」分隔的若干段,每个不满足的户一段「订单 {订单号}:{未满足项}」。
|
||||
3. **团期单在团期确认前不能单独「确认行程」**:订单详情 `confirm-itinerary` 与通用 `transition`(CONFIRM)返新码 **581065**。团期确认之后仍在定制中的户(逐户确认失败 / 确认后加入的户)可以单户补确认。
|
||||
4. **团期时间线新增事件 `BATCH_SUB_ORDER_CONFIRM_FAILED`**(子订单确认行程失败),逐户确认失败时写入,带订单号与原因。
|
||||
5. **出行完毕(`TRIP_FINISHED`)也可出具合同保险**:面板 `issuable=true`,手动开 / 重开不再返 589548。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
#8268 把「配置 → 确认」改成人工确认,但子订单仍需运营逐户点「确认行程」,而合同保险由团期确认后的「全团补发」与逐户确认链路两条路出具,存在并发重复出具风险。SRS §0.27.7 定案:
|
||||
|
||||
| 项 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 子订单确认行程 | 运营逐户点 | 团期确认提交后系统逐户确认(每户独立事务,单户失败隔离) |
|
||||
| 团期确认门 | 四项 ready + 物资已确认 | 另加逐户预检,任一户不满足整团不可确认 |
|
||||
| 合同保险出具 | 确认后全团补发 + 逐户确认链路 | 只剩逐户确认链路 |
|
||||
| 团期单单户确认 | 可以 | 团期确认前 581065;确认后可补确认 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/confirm` | 修改(门条件 + 错误 message + 副作用) | 逐户预检;589556 逐户列订单号与未满足项;成功后逐户确认行程 |
|
||||
| 2 | 确认行程 | POST | `/v3/admin/order/{id}/confirm-itinerary` | 修改(新增错误码) | 团期单在团期确认前 581065 |
|
||||
| 3 | 状态变更操作(内部通用) | POST | `/v3/admin/order/{id}/transition` | 修改(新增错误码) | `eventCode=CONFIRM` 与 #2 同一守卫,581065 |
|
||||
| 4 | 团期状态流水 | GET | `/v3/admin/order/group-batch/{groupBatchId}/status-logs` | 修改(枚举新增值) | 新事件 `BATCH_SUB_ORDER_CONFIRM_FAILED` |
|
||||
| 5 | 合同保险面板(GB-ADM-030) | GET | `/v3/admin/order/group-batch/{groupBatchId}/contracts` | 修改(出参取值) | `issuable` 在 `TRIP_FINISHED` 为 true |
|
||||
| 6 | 手动开合同 / 保险(GB-ADM-031) | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/issue` | 修改(前置门放宽) | `TRIP_FINISHED` 不再 589548 |
|
||||
| 7 | 作废重开合同 / 保险(GB-ADM-031) | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/reissue` | 修改(前置门放宽) | 同上 |
|
||||
|
||||
网关无改动(均在既有 `/v3/admin/order` 前缀下);无新增权限码;无 DB schema 变更。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期确认 `POST /v3/admin/order/group-batch/{groupBatchId}/confirm`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「配置」节点的「确认」按钮。本次起点一次确认 = 团期进入「确认」+ 全团定制中的户确认行程 + 逐户出合同保险。门不满足时,运营从 589556 的 message 里直接看到是哪几户、差什么。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500(不变) |
|
||||
|
||||
无请求体(不变)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Void | 成功返回 null(不变);团期进入 `MATERIAL_PREPARING`,子订单逐户确认在后台异步进行 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/confirm HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
接口立即返回 200,不等逐户确认完成。逐户确认在团期确认事务提交后异步执行、每户独立事务:
|
||||
|
||||
- 单户失败(如出具外的副作用异常)不回滚团期确认、不影响其他户,该户留在「定制中」;
|
||||
- 失败户写团期时间线 `BATCH_SUB_ORDER_CONFIRM_FAILED`(见接口 4),可在订单详情单户补确认;
|
||||
- 已是待出行 / 出行中的户跳过,不重复确认、不重复出具。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
门不满足(团期级 + 逐户,「;」分段):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589556,
|
||||
"message": "团期尚不满足确认条件:物资未确认;订单 HL2609240001:未付订金、未指定主报账人;订单 HL2609240002:未提交用车需求",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
只有逐户不满足时,没有团期级那一段:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589556,
|
||||
"message": "团期尚不满足确认条件:订单 HL2609240003:待支付(须先付订金或取消)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
状态不是「配置」(含重复确认):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期状态不允许当前操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判定顺序不变:589500 → 589501 → 确认门 589556 → CAS 推进;权限码 `group-batch:confirm`(不变,589507)。
|
||||
- 团期级与逐户预检**一并求值、一次列全**,不会改一项点一次。
|
||||
- 逐户预检范围:待支付、定制中的活跃户(已取消不在内);待出行 / 出行中 / 已完成的户跳过。
|
||||
- 逐户未满足项(同一户内以「、」连接):
|
||||
- `待支付(须先付订金或取消)`:订单仍在待支付;
|
||||
- `未付订金`:付款项未过——团期口径**付订金即可**,`DEPOSIT_PAID` / `FULLY_PAID` 都算满足;
|
||||
- 出行人:`未添加出行人` / `出行人信息未完善 {已完成}/{总数}(部分待补全)` / `至少需要一名成人出行人填写手机号用于合同签署`;
|
||||
- 户级房:`未提交用房需求` / `用房需求未完成(当前: {状态})`;
|
||||
- 户级车:`未提交用车需求` 或用车清单项给出的原因(整团免车 / 团车完成来源视为满足);
|
||||
- `产品未配置合同方案`;
|
||||
- `未指定主报账人`(与单户确认 581062 同一判据)。
|
||||
- 预检前系统会按团期人员配置补齐各户的团期人员副本(PR #8349),先设主报账人后下单的户不会再被误报「未指定主报账人」。
|
||||
- 团期级未满足项取值不变:`房未配齐` / `车未配齐` / `导游领队未配齐` / `摄影未配齐` / `物资未确认`。
|
||||
- 任一户不满足整团不可确认,团期与子订单状态均不变。
|
||||
|
||||
---
|
||||
|
||||
### 2. 确认行程 `POST /v3/admin/order/{id}/confirm-itinerary`
|
||||
|
||||
**VO**: `ConfirmItineraryReqVO` → `OrderTransitionRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单详情页「确认行程」按钮(定制中 → 待出行)。散客单用法不变;团期单改为随团期确认,只在团期确认后作为补确认入口使用。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | 订单主键 | 不变 |
|
||||
| reporterAssignmentId | Body | Long | 否 | 本单人员 assignmentId | 新的主报账人;不传沿用当前主报账人(不变);请求体整体可省略 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 是否成功(不变) |
|
||||
| oldStatus / newStatus | String | 变更前 / 后粗状态,如 `CUSTOMIZING` → `PENDING_DEPARTURE`(不变) |
|
||||
| oldFlowStatus / newFlowStatus | String | 变更前 / 后细状态(不变) |
|
||||
| triggeredEvents | List<String> | 副作用事件,如 `ASYNC_CONTRACT_GENERATE` / `ASYNC_INSURANCE_ISSUE`(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"reporterAssignmentId": "2072930844657283074"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"oldStatus": "CUSTOMIZING",
|
||||
"newStatus": "PENDING_DEPARTURE",
|
||||
"oldFlowStatus": "待确认",
|
||||
"newFlowStatus": "待出行",
|
||||
"triggeredEvents": ["ASYNC_CONTRACT_GENERATE", "ASYNC_INSURANCE_ISSUE"]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参。请求体可整体不传(不变):
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期单、所属团期尚未确认(招募中 / 配置 / 已流团等):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581065,
|
||||
"message": "团期单随团期确认,不可单独确认",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
其余既有码不变,例如清单未通过:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581036,
|
||||
"message": "确认订单前置校验未通过,请先补全所有必填项",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 581065 只在「定制中 + CONFIRM」这一跳判,且在五项清单(581036)、主报账人(581062)之前;散客单不判。
|
||||
- 判「团期单」走统一归团解析(`group_batch_id` 或按 `product_batch_id` 反查),与团期详情口径一致。
|
||||
- 团期已确认(`MATERIAL_PREPARING` 及之后,与合同保险出具门同一状态集)时放行:用于逐户确认失败后的重试、确认后转入 / 加入的户。
|
||||
- 团期管理员角色(`GROUP_BATCH_MANAGER`)调本接口仍被既有写守卫 581008「无权查看此订单」拒绝(不变),补确认需由有订单写权限的角色操作。
|
||||
- CONFIRM 落库改为带原状态条件的 CAS:团期驱动的逐户确认与人工补确认同时打到同一户时只有一个成功,输家报错回滚,不会重复出具合同保险。
|
||||
|
||||
---
|
||||
|
||||
### 3. 状态变更操作(内部通用) `POST /v3/admin/order/{id}/transition`
|
||||
|
||||
**VO**: `OrderTransitionReqVO` → `OrderTransitionRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
通用状态机入口(Swagger 隐藏)。本次只影响 `eventCode=CONFIRM`:与「确认行程」落到同一道 Service 层守卫,团期单在团期确认前 581065。其它事件不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | 订单主键 | 不变 |
|
||||
| eventCode | Body | String | ✅ | `PAY_DEPOSIT` / `PAY_FULL` / `CANCEL` / `CONFIRM` / `DEPART` / `FINISH` / `TERMINATE` | 本次只有 `CONFIRM` 行为变化 |
|
||||
| reason | Body | String | 否 | - | 操作原因(不变) |
|
||||
| payload | Body | Map | 否 | - | CONFIRM 可传 `reporterAssignmentId`(不变) |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 不变 |
|
||||
| oldStatus / newStatus / oldFlowStatus / newFlowStatus | String | 不变 |
|
||||
| triggeredEvents | List<String> | 不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"eventCode": "CONFIRM",
|
||||
"reason": "定制师确认所有前置条件已完成"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"oldStatus": "CUSTOMIZING",
|
||||
"newStatus": "PENDING_DEPARTURE",
|
||||
"oldFlowStatus": "待确认",
|
||||
"newFlowStatus": "待出行",
|
||||
"triggeredEvents": ["ASYNC_CONTRACT_GENERATE", "ASYNC_INSURANCE_ISSUE"]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参;非 CONFIRM 事件行为不变。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581065,
|
||||
"message": "团期单随团期确认,不可单独确认",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 与接口 2 完全同一判据与放行条件;散客单不受影响。
|
||||
- 非 CONFIRM 事件的落库仍是原写法;CONFIRM 落库改为 CAS(同接口 2)。
|
||||
|
||||
---
|
||||
|
||||
### 4. 团期状态流水 `GET /v3/admin/order/group-batch/{groupBatchId}/status-logs`
|
||||
|
||||
**VO**: `List<GroupBatchStatusLogItemVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「操作记录 / 时间线」。本次新增一个事件值:团期确认后逐户确认行程失败时每户一条。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| [].eventType | String | **新增取值** `BATCH_SUB_ORDER_CONFIRM_FAILED` |
|
||||
| [].eventTypeName | String | 新值对应「子订单确认行程失败」 |
|
||||
| [].changeType | String | 新事件为 `DATA`(团期状态不变,`fromStatus == toStatus`) |
|
||||
| [].content | String | 「订单 {订单号} 随团期确认行程失败:{原因}(该户留在定制中,可在订单详情单户补确认)」,原因最长 300 字截断 |
|
||||
| [].operatorType | String | 新事件为 `SYSTEM`,`operatorId` 为 null |
|
||||
| [].extra | Object | 新事件带 `orderId` / `orderNo` / `reason` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985/status-logs HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"changeType": "DATA",
|
||||
"eventType": "BATCH_SUB_ORDER_CONFIRM_FAILED",
|
||||
"eventTypeName": "子订单确认行程失败",
|
||||
"fromStatus": "MATERIAL_PREPARING",
|
||||
"toStatus": "MATERIAL_PREPARING",
|
||||
"content": "订单 HL2609240001 随团期确认行程失败:[581062] 请先指定本单主报账人(尾款代收人)(该户留在定制中,可在订单详情单户补确认)",
|
||||
"operatorType": "SYSTEM",
|
||||
"operatorId": null,
|
||||
"extra": {
|
||||
"orderId": "2103023889187799042",
|
||||
"orderNo": "HL2609240001",
|
||||
"reason": "[581062] 请先指定本单主报账人(尾款代收人)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
逐户全部成功时不写这类事件;无流水返回空数组(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": [], "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `eventTypeName` 由后端给,前端直接显示即可(不变的约定)。
|
||||
- `reason` 是原始失败信息:业务异常为「[错误码] 文案」,非业务异常为「异常类名: 消息」,可能含技术细节。
|
||||
- 同一原因同时写入该户 `order_main.last_confirm_error`(最长 500 字),单户补确认成功后清空;该列当前**没有接口出参**。
|
||||
- 同一户多次失败会各写一条,不去重。
|
||||
|
||||
---
|
||||
|
||||
### 5. 合同保险面板 `GET /v3/admin/order/group-batch/{groupBatchId}/contracts`
|
||||
|
||||
**VO**: `GroupBatchContractBoardVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「合同保险」页签首屏;`issuable` 决定出具按钮是否可点。本次只补 `TRIP_FINISHED`(出行完毕)为可出具。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| issuable | Boolean | **取值变化**:`TRIP_FINISHED` 由 false 改为 true;其余状态不变 |
|
||||
| batchStatus | String | 团期状态存储值(不变) |
|
||||
| totalCount / contractIssuedCount / contractSignedCount / insuranceIssuedCount | Integer | 不变 |
|
||||
| items | List | 逐户明细(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985/contracts HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"issuable": true,
|
||||
"batchStatus": "TRIP_FINISHED",
|
||||
"totalCount": 4,
|
||||
"contractIssuedCount": 4,
|
||||
"contractSignedCount": 3,
|
||||
"insuranceIssuedCount": 4,
|
||||
"items": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无活跃子订单时 `totalCount=0`、`items` 为空数组(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "issuable": true, "batchStatus": "TRIP_FINISHED", "totalCount": 0, "items": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 可出具状态集:`MATERIAL_PREPARING` / `PENDING_DEPARTURE` / `TRAVELLING` / **`TRIP_FINISHED`(新增)** / `REVIEWING` / `SETTLED`。
|
||||
- 同一状态集也用于接口 2 / 3 的「团期已确认、放行补确认」判断,保证确认得了的户也出具得了。
|
||||
|
||||
---
|
||||
|
||||
### 6. 手动开合同 / 保险 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/issue`
|
||||
|
||||
**VO**: `GroupBatchContractIssueReqVO` → `GroupBatchIssueResultVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「合同保险」页签逐户开合同 / 保险。本次只放宽前置门:出行完毕的团(返团当天补签 / 补开)不再被 589548 挡住。入参出参结构不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
| orderIds | Body | List<Long> | 否 | - | 为空 = 本期全部尚未出具的户(不变) |
|
||||
| target | Body | String | ✅ | `CONTRACT` / `INSURANCE` / `BOTH` | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| totalCount / successCount / failCount / skipCount | Integer | 不变 |
|
||||
| results[].orderId / target / outcome / message | String | 不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "orderIds": ["2103023889187799042"], "target": "CONTRACT" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"totalCount": 1,
|
||||
"successCount": 1,
|
||||
"failCount": 0,
|
||||
"skipCount": 0,
|
||||
"results": [
|
||||
{ "orderId": "2103023889187799042", "target": "CONTRACT", "outcome": "SUCCESS", "message": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
已出具的户逐项 `SKIPPED`(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "totalCount": 1, "successCount": 0, "failCount": 0, "skipCount": 1, "results": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期尚未确认(`RECRUITING` / `RESOURCE_PREPARING` 等,不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589548,
|
||||
"message": "团期确认后才能出具合同与保险",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `TRIP_FINISHED` 改前 589548,改后放行;其余状态口径不变。
|
||||
- 自动出具只剩逐户确认链路;本接口仍是人工补出的通路(逐户确认失败户补确认后会自动出,一般无需手动)。
|
||||
|
||||
---
|
||||
|
||||
### 7. 作废重开合同 / 保险 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/reissue`
|
||||
|
||||
**VO**: `GroupBatchContractIssueReqVO` → `GroupBatchIssueResultVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「开错」态的补救通路。前置门与手动开一致,本次同样放宽到出行完毕。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
| orderIds | Body | List<Long> | 否 | - | 不变 |
|
||||
| target | Body | String | ✅ | `CONTRACT` / `INSURANCE` / `BOTH` | 不变 |
|
||||
| reason | Body | String | 否 | - | 作废原因(不变) |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| totalCount / successCount / failCount / skipCount | Integer | 不变 |
|
||||
| results[] | List | 逐户结果(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{ "orderIds": ["2103023889187799042"], "target": "CONTRACT", "reason": "方案选错" }
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"totalCount": 1,
|
||||
"successCount": 1,
|
||||
"failCount": 0,
|
||||
"skipCount": 0,
|
||||
"results": [
|
||||
{ "orderId": "2103023889187799042", "target": "CONTRACT", "outcome": "SUCCESS", "message": null }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
作废失败的户记 `FAILED`,不影响其他户(不变):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "totalCount": 1, "successCount": 0, "failCount": 1, "skipCount": 0, "results": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589548,
|
||||
"message": "团期确认后才能出具合同与保险",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 与手动开共用同一出具门;`TRIP_FINISHED` 放行。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 调用 |
|
||||
|------|------|
|
||||
| ✅ 团期单确认行程 | 补齐各户资料 → 团期 `confirm` → 系统逐户确认,合同保险自动出 |
|
||||
| ❌ 团期确认前逐户点确认行程 | `confirm-itinerary` → 581065 |
|
||||
| ✅ 团期确认后某户失败 | 看时间线 `BATCH_SUB_ORDER_CONFIRM_FAILED` 的原因 → 补资料 → 订单详情 `confirm-itinerary` 补确认 |
|
||||
| ❌ 确认后等「全团补发」合同 | 已下线;合同保险只随逐户确认自动出具 |
|
||||
|
||||
### 589556 的解析
|
||||
|
||||
message 冒号之后按「;」切段:不以「订单 」开头的是团期级(「、」分隔的五种之一),以「订单 {订单号}:」开头的是逐户段,冒号后按「、」切出未满足项。可直接逐段展示,无需解析。
|
||||
|
||||
### 581065 的处理
|
||||
|
||||
提示「请到团期详情点确认」,不要引导运营去补本单资料(那是 581036 的出路)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端动作 | 外部可观察的写入 |
|
||||
|----------|------------------|
|
||||
| `confirm` 被拒(589556 等) | 零写入(预检前的人员副本补齐随事务一并回滚) |
|
||||
| `confirm` 成功(团期事务) | 团期状态 CAS 推进 + 时间线 `BATCH_CONFIRM`;各户团期人员副本(`order_staff_assignment`)按团期配置补齐 |
|
||||
| `confirm` 成功(提交后逐户,每户独立事务) | 子订单 `CUSTOMIZING → PENDING_DEPARTURE`、写 `confirmed_at`、订单日志 CONFIRM「随团期确认行程」、应付台账推送;随后逐户生成合同 / 保单 |
|
||||
| 逐户确认失败 | 该户不变;团期时间线 `BATCH_SUB_ORDER_CONFIRM_FAILED` + `order_main.last_confirm_error` |
|
||||
| 单户补确认成功 | 同上逐户写入,并清空 `last_confirm_error` |
|
||||
| `confirm-itinerary` / `transition` 被 581065 拒 | 零写入 |
|
||||
|
||||
无 schema 变更(`last_confirm_error` 为既有列)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截);团期不存在 → 589500。
|
||||
- 待支付户、未付订金户会挡住整团确认:须先付订金或取消该户。
|
||||
- 已是待出行 / 出行中的户在团期确认时跳过,不重复确认、不重复出具。
|
||||
- 逐户确认使用系统身份,订单日志 / 合同创建人为 SYSTEM;确认人记在团期时间线 `BATCH_CONFIRM`。
|
||||
- 定案前已单独确认但未出合同的存量户,需在「合同保险」页签手动补出。
|
||||
- 建单时团期人员副本连同报账人等级一起抄(PR #8349),后挂团的户订单层主报账人不再停在 NONE。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### 时间线事件(GroupBatchLogEventType,新增一值)
|
||||
|
||||
**所属字段**: 团期状态流水 `eventType` / `eventTypeName` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `BATCH_SUB_ORDER_CONFIRM_FAILED` | 子订单确认行程失败 | 本次新增;`changeType=DATA`,`operatorType=SYSTEM`,extra 带 orderId / orderNo / reason |
|
||||
|
||||
### 错误码
|
||||
|
||||
| 码 | 文案 | 说明 |
|
||||
|----|------|------|
|
||||
| 581065 | 团期单随团期确认,不可单独确认 | 本次新增 |
|
||||
| 589556 | 团期尚不满足确认条件:{0} | 码值不变,{0} 改为「;」分段、含逐户段 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 589556 message `{0}` | 团期级未满足项,「、」连接 | 「;」分段:团期级一段(有才出现)+ 每户一段「订单 {订单号}:{未满足项}」 |
|
||||
| `confirm-itinerary` / `transition(CONFIRM)` 错误码 | 无 581065 | 团期单在团期确认前 581065 |
|
||||
| 时间线 `eventType` | 无该值 | 新增 `BATCH_SUB_ORDER_CONFIRM_FAILED` |
|
||||
| 合同保险面板 `issuable`(`TRIP_FINISHED`) | false | true |
|
||||
| `contracts/issue` / `reissue`(`TRIP_FINISHED`) | 589548 | 放行 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期确认后子订单 | 仍在定制中,需逐户点确认行程 | 系统逐户确认 → 待出行 |
|
||||
| 合同保险出具 | 确认后全团补发 + 逐户确认链路 | 只剩逐户确认链路 |
|
||||
| 团期确认门 | 四项 ready + 物资 | 另加逐户预检(付订金即可) |
|
||||
| 子订单 CONFIRM 落库 | 按主键更新 | 带原状态条件 CAS |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 是。按「、」切 589556 message 的前端会把逐户段切碎;团期单的「确认行程」在团期确认前会收到 581065。
|
||||
- **前端是否必须同步上线**: 建议同步。后端不依赖前端即可工作(团期确认即完成子订单确认);前端未改时只是提示不友好、按钮可点但报 581065。
|
||||
- **前端 workaround 清理点**: 团期单订单详情「确认行程」按钮在团期确认前隐藏或置灰;589556 改按「;」分段展示;时间线对新事件无需映射(后端给中文名)。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期确认门与确认后的子订单联动、团期单 CONFIRM 守卫、团期时间线新事件、合同保险出具门的状态集。
|
||||
- **零影响**:
|
||||
- 散客单的确认行程与 transition 各事件
|
||||
- 团期确认的权限码、路径、入参、成功出参
|
||||
- `GET /v3/admin/order/{id}/confirm-checklist` 的入参出参(逐户预检复用其判据,接口本身不变)
|
||||
- 进入待出发的七项硬门
|
||||
- 小程序端
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-order-service-v3 = dev-v3 @ f55840cb5(PR #8346,第一轮 2026-09-24 15:30–16:16),复验 dev-v3 @ 9ffb3c2de(PR #8349,2026-09-24)的 TEST 环境实测(证据见工单 #8339 验收评论);工单 #8339 已验收关单。
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 全户满足,调团期 `confirm` | 200;子订单 定制中 → 待出行,写 `confirmed_at`,订单日志 CONFIRM「随团期确认行程」;合同 1 份 SIGNED、保险 2 条 INSURED / 户,无重复(两轮均验) |
|
||||
| 2 | 某户仅付订金(DEPOSIT_PAID) | 视为满足付款项 |
|
||||
| 3 | 待支付 / 户级房未完成 / 无主报账人(第一轮);产品未配置合同方案 / 成人出行人无手机号 / 未提交用车需求(复验,SQL 构造已恢复) | 589556 整团拒,逐户列订单号 + 未满足项,团期与子订单状态不变 |
|
||||
| 4 | 已待出行户 | 跳过,不重复确认、不重复出具 |
|
||||
| 5 | CHECK 约束注入使一户逐户确认失败 | 团期确认不回滚、其他户正常;时间线 `BATCH_SUB_ORDER_CONFIRM_FAILED`(含订单号 + 原因),`last_confirm_error` 有值;删约束后单户补确认成功,`last_confirm_error` 清空 |
|
||||
| 6 | 团期单 `confirm-itinerary` / `transition` CONFIRM(团期未确认) | 581065;散客单不受影响 |
|
||||
| 7 | `TRIP_FINISHED`(附带 `REVIEWING`)调 `GET /contracts` | `issuable=true` |
|
||||
| 8 | 先设 PRIMARY 后下单的户 / 存量 NONE 副本(PR #8349) | 建单即抄为 PRIMARY;确认预检前补齐,不再误报「未指定主报账人」 |
|
||||
|
||||
本地:788 例 + 合入后 606 例(含 6 个 Docker IT,唯一红为 dev-v3 既有基线 `ConfirmChecklistGuardIT` 581062);PR #8349 相关 360 例(含 Docker IT)0 失败。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #8302 | #8268 | 团期人工确认端点;确认后全团补发合同保险 | ⚠️ 确认端点有效;「确认后全团补发」被本单下线 |
|
||||
| **本 PR #8346** | **#8339** | 确认门逐户预检、子订单随团期确认、581065 | ✅ 最新 |
|
||||
| **本 PR #8349** | **#8339** | 人员副本补齐、逐户确认失败留痕 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8339](https://git.1814.love/wx/HL/issues/8339)
|
||||
- 关联 PR: [wx/HL#8346](https://git.1814.love/wx/HL/pulls/8346)、[wx/HL#8349](https://git.1814.love/wx/HL/pulls/8349)
|
||||
- 定案文档:SRS §0.27.7(PR #8338)
|
||||
- 同批条目:团期核单 / 结算 / 反结算同步子订单(#8341);团期人工确认端点(#8268)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8339](https://git.1814.love/wx/HL/issues/8339)
|
||||
- **PR**: [#8346](https://git.1814.love/wx/HL/pulls/8346)、[#8349](https://git.1814.love/wx/HL/pulls/8349)
|
||||
- **Merge commit**: [f55840cb5](https://git.1814.love/wx/HL/commit/f55840cb5dca1d2f3a81078548c7a50cc813ca7e)、[9ffb3c2de](https://git.1814.love/wx/HL/commit/9ffb3c2de31ccf2ba7b16c2922f6f493f2025269)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,504 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8341"
|
||||
title: "团期核单 / 结算 / 反结算同步子订单:进入核单时待核单户进核单中;团期结算即整团财务复核(逐户结算 + 报账单),有逐户核单未提交的户返 589568 并列出订单 ID;反结算时已结算户退回待结算"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "d2585f3c766c9ad7e102f7409f1a7419792636dd"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "六节点定案(SRS §0.27.7):团期单子订单的核单结算状态由团期驱动,与团期状态变更同一事务、全有全无。① 团期进入核单(POST .../review/start 或首次 POST .../settlement/cost 把团期从 TRIP_FINISHED 推到 REVIEWING):在团户中 flow=PENDING_REVIEW 的户同步为 review IN_PROGRESS / flow REVIEWING,已在核单中或已提交的户不动,幂等命中(alreadyStarted=true / 第二笔成本)不同步。② 团期结算 POST .../settle = 整团财务复核:待结算户逐户做与「财务复核确认结算」相同的写入(settlement COMPLETED / flow SETTLED / settled_at、SETTLEMENT_CONFIRM 日志、推送 BZ 报账单),已结算户跳过;有逐户核单未提交(或定稿后被逐户反确认)的户,整团拒绝 589568「整团核单当前状态不允许该操作:结算需要所有在团订单逐户核单已提交,未提交订单:{订单 ID,「、」分隔}」,零写入。③ 团期反结算 POST .../settle/reopen:已结算户退回 flow PENDING_SETTLE / settlement PENDING、清 settled_at,review 与逐户核单快照不动,已生成的报账单不撤(重新结算按 settlementId 幂等命中)。路径、入参、成功出参均不变;已取消户不受影响。前端需:处理团期结算的 589568 新原因(提示先去对应订单提交逐户核单);团期结算后不必再引导运营逐户点「结算确认」。 前端已交付并验证:实证对 589568 零按码分支(核团域同码不同场景均透 message),验团成功文案无「引导逐户结算确认」残留,透 message 即达标;settle/reopen/review-start JSDoc 补同步语义与 589568 新原因(禁解析订单 ID 自建链接),AuditTab 注释订正。纯文档零行为改动,hl-admin@d2585f3c。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期结算: 核单 / 结算 / 反结算同步子订单(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8345
|
||||
> **Issue**: #8341
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期详情「核单 / 结算」节点的发起核单、录共享成本、结算、反结算按钮;子订单列表与订单详情里的核单 / 结算状态展示
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **团期结算 = 整团财务复核**。点团期「结算」后,在团待结算的户一并变为「已结算」并各生成一张报账单,运营**不再逐户点结算确认**。
|
||||
2. **团期结算新增一种 589568 拒绝原因**:有户的逐户核单还没提交时,整团拒绝,message 列出这些户的**订单 ID**。
|
||||
3. **团期反结算会把子订单一并退回待结算**;已生成的报账单保留。
|
||||
4. **团期进入核单时,待核单的户自动进入核单中**,不必等逐户第一次录核单明细。
|
||||
|
||||
以上四个接口的路径、入参、成功出参都不变。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
改前团期的核单、结算、反结算只改团期自身,子订单仍要逐户推进:团期「已结算」而子订单还「待结算」、财务侧没有报账单的情况会出现。SRS §0.27.7 定案子订单跟随团期:
|
||||
|
||||
| 团期动作 | 子订单同步(同一事务) |
|
||||
|---|---|
|
||||
| 进入核单(`TRIP_FINISHED → REVIEWING`) | `flow PENDING_REVIEW` 的户 → review `IN_PROGRESS` / flow `REVIEWING` |
|
||||
| 逐户核单明细录入与提交 | 照旧(提交后 flow `PENDING_SETTLE`、review `COMPLETED`、settlement `PENDING`) |
|
||||
| 结算(`REVIEWING → SETTLED`) | 待结算户逐户财务复核 → settlement `COMPLETED` / flow `SETTLED` / `settled_at` + 报账单 |
|
||||
| 反结算(`SETTLED → REVIEWING`) | 已结算户 → flow `PENDING_SETTLE` / settlement `PENDING`,清 `settled_at` |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 结算归档 | POST | `/v3/admin/order/group-batch/{groupBatchId}/settle` | 修改(新增拒绝原因 + 副作用) | 逐户结算 + 报账单;有逐户核单未提交的户 589568 |
|
||||
| 2 | 结算反确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/settle/reopen` | 修改(副作用) | 已结算户退回待结算 |
|
||||
| 3 | 发起核单 | POST | `/v3/admin/order/group-batch/{groupBatchId}/review/start` | 修改(副作用) | 首次推进时待核单户进核单中 |
|
||||
| 4 | 录入团期共享成本 | POST | `/v3/admin/order/group-batch/{groupBatchId}/settlement/cost` | 修改(副作用 + 事务) | 首笔成本推进团期时同上同步;整方法改为单事务 |
|
||||
|
||||
网关无改动;无新增权限码;无新增错误码;无 DB schema 变更。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 结算归档 `POST /v3/admin/order/group-batch/{groupBatchId}/settle`
|
||||
|
||||
**VO**: `GroupBatchSettleReqVO` → `Result<Void>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「核单」节点的「结算」按钮(`REVIEWING → SETTLED`)。本次起同时完成全团子订单的财务复核,逐户生成报账单。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
| checkNote | Body | String | 否 | - | 结算意见(请求体整体可省略,不变);本次起也写进逐户财务复核日志的备注 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Void | 成功返回 null(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"checkNote": "成本已逐项核对"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参。团期没有在团子订单时跳过子订单同步,团期照常结算:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
有在团户逐户核单未提交(本次新增的原因,码值与前缀不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589568,
|
||||
"message": "整团核单当前状态不允许该操作:结算需要所有在团订单逐户核单已提交,未提交订单:2103023889187799042",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
已结算归档(不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589555,
|
||||
"message": "该团期已结算归档,不可重复结算",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判定顺序:判权(589507)→ 团期状态(589555 / 589501)→ 整团核单前置(589567 / 589568,不变)→ 团期 CAS → **子订单同步** → 时间线。
|
||||
- 在团户分三类:待结算(flow `PENDING_SETTLE` + settlement `PENDING` + review `COMPLETED`)逐户复核;已结算(flow `SETTLED` + settlement `COMPLETED`)跳过、不重复推报账单;其余一律视为「逐户核单未提交」→ 589568。
|
||||
- 589568 在任何逐户写入之前判,拒绝时零写入;列出的是**订单 ID**(不是订单号),以「、」分隔。
|
||||
- 逐户复核的写入与副作用与 `POST /v3/admin/order/{orderId}/settlement/confirm`(财务复核)同源;任一户复核失败,其业务码原样返回,整团回滚(含已推的报账单)。
|
||||
- 团期结算不对每户再做订单数据范围判定:财务角色结算整团不会被逐户 581008 挡住;判权仍是团期结算原有的角色判据。
|
||||
- 已取消户不在「在团户」内,全程不受影响。
|
||||
|
||||
---
|
||||
|
||||
### 2. 结算反确认 `POST /v3/admin/order/group-batch/{groupBatchId}/settle/reopen`
|
||||
|
||||
**VO**: `Result<Void>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「结算」节点的「反结算」按钮(`SETTLED → REVIEWING`)。本次起已结算的子订单一并退回待结算。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
无请求体(不变)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Void | 成功返回 null(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/settle/reopen HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无列表出参;没有已结算子订单时只退团期,不报错:
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": null, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期当前不是已结算(不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期状态不允许当前操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只退「财务复核」这一步:flow `SETTLED → PENDING_SETTLE`、settlement `COMPLETED → PENDING`、清 `settled_at`;review 保持 `COMPLETED`,逐户核单终态快照与核单汇总不动。
|
||||
- 不在已结算的户(未结算 / 已被逐户反确认)不命中,不写。
|
||||
- **已生成的报账单不撤**;重新结算时按核单汇总 ID 幂等命中既有报账单,不重复生成。反结算期间出纳仍可能处理这些报账单。
|
||||
- 本接口未新增错误码。
|
||||
|
||||
---
|
||||
|
||||
### 3. 发起核单 `POST /v3/admin/order/group-batch/{groupBatchId}/review/start`
|
||||
|
||||
**VO**: `GroupBatchReviewStartRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「出行完毕」节点的「发起核单」按钮(`TRIP_FINISHED → REVIEWING`)。本次起首次推进时同事务把待核单的户带进核单中。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
|
||||
无请求体(不变)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期主订单 ID(不变) |
|
||||
| batchStatus | String | 恒为 `REVIEWING`(不变) |
|
||||
| batchStatusDesc | String | 恒为「核单中」(不变) |
|
||||
| alreadyStarted | Boolean | true = 幂等命中,零写入(不变;本次起幂等命中也不同步子订单) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/review/start HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchStatus": "REVIEWING",
|
||||
"batchStatusDesc": "核单中",
|
||||
"alreadyStarted": false
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
重复点击返回 `alreadyStarted=true`,团期与子订单都不写(不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "groupBatchId": "2097250563497385985", "batchStatus": "REVIEWING", "batchStatusDesc": "核单中", "alreadyStarted": true },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不在出行完毕(不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589564,
|
||||
"message": "当前团期状态不可发起核单(须为「出行完毕」)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只推 flow `PENDING_REVIEW` 的户(review `PENDING` / `IN_PROGRESS`、settlement 为空或 `NONE`);已在核单中、已提交、已结算的户不动。
|
||||
- flow 仍早于待核单的户(如未完成出行同步的存量户)跳过并记日志,不阻断团期进入核单;这些户日后第一次录核单明细时仍会自己进入核单中。
|
||||
- 权限码 `group-batch:finance:advance` 与各错误码(589507 / 589564 / 589565 / 589566)不变。
|
||||
|
||||
---
|
||||
|
||||
### 4. 录入团期共享成本 `POST /v3/admin/order/group-batch/{groupBatchId}/settlement/cost`
|
||||
|
||||
**VO**: `RecordBatchCostReqVO` → `Result<Long>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期核单页录大巴 / 领队 / 摄影等共享成本。团期在出行完毕时录第一笔会顺带把团期推进核单中;本次起这一推进同时同步子订单(与接口 3 同一逻辑)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不变 |
|
||||
| costType | Body | String | ✅ | `BUS` / `LEADER` / `PHOTOGRAPHER` / `OTHER` | 不变 |
|
||||
| amount | Body | BigDecimal | ✅ | ≥ 0 | 不变 |
|
||||
| source | Body | String | 否 | `MANUAL` / `FLEET_CALLBACK` | 默认 `MANUAL`(不变) |
|
||||
| sourceRefNo | Body | String | 否 | `FLEET_CALLBACK` 时必填 | 幂等键(不变) |
|
||||
| remark | Body | String | 否 | ≤ 255 字符 | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | Long | 新成本明细 ID(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"costType": "BUS",
|
||||
"amount": 1200.00,
|
||||
"source": "MANUAL",
|
||||
"remark": "大巴费用"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": 90412,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
第二笔及以后的成本(团期已在核单中)只插明细,不再同步子订单(不变的推进口径):
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": 90413, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不在可录成本的状态(不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589501,
|
||||
"message": "团期状态不允许当前操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 本接口由「无事务」改为整方法单事务:团期推进、子订单同步、明细插入全有全无(改前 CAS 与 INSERT 各自提交,插入失败时团期可能已被推进)。
|
||||
- 权限码 `group-batch:finance:advance` 与错误码不变。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用对照
|
||||
|
||||
| 场景 | 调用 |
|
||||
|------|------|
|
||||
| ✅ 团期结算 | 各户提交逐户核单 → 整团核单提交核算(ALLOCATED)→ `settle`,子订单一并结算 |
|
||||
| ❌ 有户逐户核单未提交就结算 | `settle` → 589568「…未提交订单:{订单 ID}」 |
|
||||
| ❌ 团期结算后再逐户点「结算确认」 | 不再需要;已结算户逐户复核会被既有状态门拒绝 |
|
||||
| ✅ 结算后发现问题 | `settle/reopen` → 改整团核单 → 重新 `settle`(报账单幂等,不重复) |
|
||||
|
||||
### 589568 的区分
|
||||
|
||||
589568 码值同时用于「整团核单状态不对」与本次新增的「逐户核单未提交」。前端可直接展示 message;如要分支,message 含「未提交订单:」即为本次新增原因,出路是去对应订单提交逐户核单。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 前端动作 | 外部可观察的写入(同一事务) |
|
||||
|----------|------------------|
|
||||
| `review/start` 首次成功 / 首笔 `settlement/cost` | 团期 `REVIEWING` + 待核单户 review `IN_PROGRESS` / flow `REVIEWING`(CAS,按行) |
|
||||
| `review/start` 幂等命中 | 零写入 |
|
||||
| `settle` 成功 | 核单 `CHECKED` + 团期 `SETTLED` + 待结算户 settlement `COMPLETED` / flow `SETTLED` / `settled_at`、订单日志 `SETTLEMENT_CONFIRM`(备注「团期结算(整团财务复核)[:结算意见]」)、每户一张 BZ 报账单 |
|
||||
| `settle` 被 589568 拒 | 零写入 |
|
||||
| `settle/reopen` 成功 | 团期 `REVIEWING` + 核单退回 + 已结算户 flow `PENDING_SETTLE` / settlement `PENDING` / 清 `settled_at`;报账单保留 |
|
||||
|
||||
无 schema 变更。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截);团期不存在 → 589500。
|
||||
- 已取消户全程不受影响;重复触发不重复写(各写入均带状态 CAS)。
|
||||
- 二次结算(反结算后再结算)报账单不重复、`settlement_id` 不变。
|
||||
- 首次结算后逐户反确认会被 584330(报账单已进入出纳)拒绝(既有行为,不变)。
|
||||
- 逐户财务复核入口对团期单仍开放;已被逐户复核过的户在团期结算时跳过。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `settle` 589568 message | 仅整团核单状态类原因 | 新增「结算需要所有在团订单逐户核单已提交,未提交订单:{订单 ID}」 |
|
||||
| 子订单 `settlement_status` / `flow_status` / `settled_at`(团期结算后) | 不变(待结算) | `COMPLETED` / `SETTLED` / 结算时间 |
|
||||
| 子订单 review / flow(团期进入核单后) | `PENDING` / `PENDING_REVIEW` | `IN_PROGRESS` / `REVIEWING` |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期结算 | 只改团期与整团核单 | 另做整团财务复核,逐户结算 + 报账单 |
|
||||
| 团期反结算 | 只改团期与整团核单 | 另把已结算户退回待结算 |
|
||||
| 团期进入核单 | 不碰子订单 | 待核单户进核单中 |
|
||||
| 录共享成本事务 | 无事务 | 单事务 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 部分。路径、入参、成功出参不变;`settle` 多一种 589568 原因,逐户核单未提交的团原先能结算、现在被拒。
|
||||
- **前端是否必须同步上线**: 否。后端已自行完成子订单同步;前端未改时 589568 按原样弹 message 即可理解。
|
||||
- **前端 workaround 清理点**: 团期结算后引导运营逐户「结算确认」的提示可移除;子订单列表刷新即可看到已结算。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期发起核单、首笔共享成本、结算、反结算四个动作对子订单的联动。
|
||||
- **零影响**:
|
||||
- 逐户核单明细录入与提交(`/v3/admin/order/{orderId}/settlement/**`)
|
||||
- 逐户财务复核 `POST /v3/admin/order/{orderId}/settlement/confirm` 的入参出参与判权
|
||||
- 整团核单(核算、分摊)各端点
|
||||
- 散客单的核单结算
|
||||
- 小程序端
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-order-service-v3 = dev-v3 @ f55840cb5(含 PR #8345),TEST 环境实测 2026-09-24 16:05–16:11(证据见工单 #8341 验收评论);工单 #8341 已验收关单。
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 团期进入核单 | 活跃户 review `IN_PROGRESS` / flow `REVIEWING`;重复调用状态不变 |
|
||||
| 2 | 逐户核单定稿 + 整团核单 ALLOCATED 后团期结算 | 四户 settlement `COMPLETED` / flow `SETTLED` / `settled_at`;报账单每户 1 张(BZ-202609240001~0004) |
|
||||
| 3 | 团期反结算 | 四户回 flow `PENDING_SETTLE` / settlement `PENDING`,`settled_at` 清空;报账单保留 |
|
||||
| 4 | 已取消户 / 二次结算 | 已取消户全程未动;二次结算报账单不重复、`settlement_id` 不变 |
|
||||
| 5 | 逐户核单未提交时整团结算(SQL 构造) | 589568「…未提交订单:2103023889187799042」,整团回滚 |
|
||||
|
||||
本地:相关单测 / H2 IT / ArchUnit 314 例 0 失败;Docker IT `GroupBatchAuditVersionCasMySqlIT` 10 例、`FlowStatusP1cIntegrationTest` 4 例 0 失败。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| — | #7456 | 团期结算归档 / 反确认端点 | ✅ 端点不变,本单加子订单联动 |
|
||||
| — | #7527 | 发起核单端点 | ✅ 端点不变,本单加子订单联动 |
|
||||
| **本 PR #8345** | **#8341** | 核单 / 结算 / 反结算同步子订单 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8341](https://git.1814.love/wx/HL/issues/8341)
|
||||
- 关联 PR: [wx/HL#8345](https://git.1814.love/wx/HL/pulls/8345)
|
||||
- 定案文档:SRS §0.27.7(PR #8338)
|
||||
- 同批条目:团期确认联动子订单确认行程(#8339)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8341](https://git.1814.love/wx/HL/issues/8341)
|
||||
- **PR**: [#8345](https://git.1814.love/wx/HL/pulls/8345)
|
||||
- **Merge commit**: [454e2da4b](https://git.1814.love/wx/HL/commit/454e2da4ba347d45c25834f12d290d7689b85a3a)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8343"
|
||||
title: "团单不提供用餐:按订单ID的用餐接口拒绝团单"
|
||||
consumer: "admin"
|
||||
author: "lc(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "9cec969365fbb6c059cc2d1271dcecd68f49e3e9"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "已部署 TEST 并经 Gateway 实测通过。团单(订单详情 main.groupOrder=true)只传 orderId 调用餐查询、生成、保存、重置、套用模版 5 个接口,统一返回 589614「团单不提供用餐」,不写库;散客单不变;只传 groupBatchId 的团期用餐不变。 前端已交付并验证:v3Adapter 透传 main.groupOrder,MealTab isGroupOrderMeal 守卫——订单维度且团单时不发起用餐 1/2/3/4/7 号共 5 接口,页签只渲染空态说明,编辑工具条同隐;团期维度与散客单不变。MealTab spec 27 例全绿(新增 2 例),hl-admin@9cec9693。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团单不提供用餐,按订单ID的用餐接口拒绝团单
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8347
|
||||
> **Issue**: #8343
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台订单用餐(按订单维度)的查询、生成、保存、重置与套用模版
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- 🆕 **团单报错**:订单是团单时,下表 5 个接口只传 `orderId` 调用,统一返回业务错误 `589614`「团单不提供用餐」,不写库。**查询也报错,不返回空列表。**
|
||||
- ℹ️ **判团字段**:订单详情 `GET /v3/admin/order/{id}` 的 `main.groupOrder`(`true` 为团单),与订单列表「订单类型=团单」同口径;该字段本次不改。
|
||||
- ℹ️ **不受影响**:散客单(`main.groupOrder=false`)行为不变;只传 `groupBatchId` 的团期用餐不变;存为模版(4.2)不变。
|
||||
- ℹ️ **报错先后**:订单不存在、无权(581045 / 581008)、已完成或已取消(589607)仍按原错误码先报,之后才判团单。
|
||||
- 🔁 **与 #8230 不同**:#8230 起团期子订单可按 `orderId` 保存、套用用餐,本次起团单这样调用返回 589614。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 查询用餐信息列表(3.1) | GET | `/v3/admin/order/meal-info/list` | 修改 | 只传 `orderId` 且为团单 → 589614 |
|
||||
| 2 | 生成用餐信息(3.2) | POST | `/v3/admin/order/meal-info/generate` | 修改 | 同上 |
|
||||
| 3 | 保存用餐信息(整单,3.3) | POST | `/v3/admin/order/meal-info/save` | 修改 | 同上 |
|
||||
| 4 | 重置用餐信息(3.4) | POST | `/v3/admin/order/meal-info/reset` | 修改 | 同上 |
|
||||
| 5 | 套用用餐模版(4.3) | POST | `/v3/admin/order/meal-template/apply` | 修改 | 同上 |
|
||||
|
||||
入参、出参字段不变。
|
||||
|
||||
---
|
||||
|
||||
## 三、错误码
|
||||
|
||||
| 错误码 | 文案 | 触发 |
|
||||
|---|---|---|
|
||||
| 589614 | 团单不提供用餐 | 上表 5 个接口只传 `orderId`,该订单是团单 |
|
||||
|
||||
---
|
||||
|
||||
## 四、前端动作
|
||||
|
||||
- 订单详情按 `main.groupOrder=true` 判团单,团单不提供用餐,不调用上表 5 个接口。
|
||||
- 收到 589614 时按普通业务错误提示文案。
|
||||
|
||||
---
|
||||
|
||||
## 联系人
|
||||
|
||||
@lc
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8350"
|
||||
title: "团期子订单调整放开增删出行人并同步已报名人数,禁止改出行日期与行程(新增 587045)"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "79f4a9bfbc640968029e4bfe923e9f4fb2e9a5a9"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-26"
|
||||
status_note: "PR #8353 已合并 dev-v3(8df1c0e84)并滚动部署 TEST 双实例。调整订单统一提交对团期子订单:放开增删出行人(原 587036 退役),差价按团期报价原态/终态求差,同事务同步团期已报名人数 enrolled_people(加人不校验上限);真正改出发日或提交行程一律返回新码 587045,#7325 改回团期出发日出口下线(587039~587042 退役)。散客单不变。前端待办:去掉出行人页签「团期子订单暂不支持调整出行人」提示,团期子订单出行日期/行程页签置灰且不回传未改动的 itinerary.days。 前端已交付(79f4a9bf):出行人 tab 放开(587036 退役)、出行日期/行程两 tab 对团期子订单整页置灰+587045 同口径文案、#7325b 改期出口退役、提交不再携带 itinerary.days;spec 47 例全绿。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单调整模块: 团期子订单放开增删出行人并同步已报名人数,禁止改出行日期与行程
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8353
|
||||
> **Issue**: #8350
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台「调整订单」弹窗对团期子订单的出行人 / 出行日期 / 行程三个页签
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:团期子订单在调整弹窗里**可以增删出行人**,**不能改出行日期和行程**——与上版正好相反。
|
||||
- 前端以前以为的:团期子订单增删出行人会被 `587036` 拒;改期只能改回团期出发日(`587039`~`587042`),行程可以随便提交。
|
||||
- 实际新行为:增删出行人返回 200,差价按团期报价求差,团期「已报名人数」同步增减;团期子订单只要真正改了出发日(**包括改成团期出发日**)或带了 `updates.itinerary.days`,一律返回新码 `587045`,整笔零写入。`587036`、`587039`~`587042` 不再返回。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 调整订单统一提交 | POST | `/v3/admin/order/{id}/adjustment/submit` | 行为变更 + 新增错误码 | 团期子订单放开增删出行人、同步已报名人数;改出发日 / 提交行程返回 587045 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 调整订单统一提交 `POST /v3/admin/order/{id}/adjustment/submit`
|
||||
|
||||
**VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理端「调整订单」弹窗一次性提交出行人 / 出行日期 / 行程 / 酒店 / 车辆等子领域改动。本次只改变**团期子订单**在出行人、出行日期、行程三个子领域的放行规则;请求体与响应体结构均未变,散客单行为不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
> 仅列与本次改动相关的字段,其余子领域字段结构未变。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| Authorization | Header | String | ✅ | - | 管理端登录令牌 |
|
||||
| id | Path | Long | ✅ | 订单 ID | 目标订单 |
|
||||
| updates.travelers.add | Body | Array\<TravelerEditVO\> | ❌ | 字段同出行人编辑 | 新增出行人;**团期子订单本次起允许** |
|
||||
| updates.travelers.remove | Body | Array\<Long\> | ❌ | 出行人 ID | 删除出行人;**团期子订单本次起允许** |
|
||||
| updates.travelers.update | Body | Array\<TravelerEditVO\> | ❌ | 带 id | 修改出行人资料(团期子订单原本就允许) |
|
||||
| updates.schedule.departDate | Body | String | ❌ | `yyyy-MM-dd` | **团期子订单**:与当前出发日不同即返回 587045(改成团期出发日也拒);同值回传不拦 |
|
||||
| updates.itinerary.days | Body | Array | ❌ | - | **团期子订单**:非 null 即返回 587045;`itinerary` 为空对象(`days` 为 null)不拦 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | Boolean | 提交是否成功;失败由全局异常处理器返回 `Result{code,message,data:null}` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
团期子订单新增 1 名成人:
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": {
|
||||
"travelers": {
|
||||
"add": [
|
||||
{ "name": "张三", "gender": "1", "birthday": "1990-01-01", "idType": "ID_CARD", "idNo": "110101199001011234", "phone": "13800000000" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口无空数据场景。团期报价服务不可用时不降级,整笔拒绝(见错误响应 581032)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "data": { "success": true }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期子订单改出发日(含改成团期出发日)或提交行程(**本次新增**):
|
||||
|
||||
```json
|
||||
{ "code": 587045, "message": "团期子订单不支持在子订单中调整出行日期和行程,如需改期请走团期转期或联系团期管理员", "success": false, "data": null }
|
||||
```
|
||||
|
||||
团期子订单人数变化、团期报价获取失败(存量码,本接口新可达):
|
||||
|
||||
```json
|
||||
{ "code": 581032, "message": "团期报价获取失败", "success": false, "data": null }
|
||||
```
|
||||
|
||||
团期子订单加人时所属团期已流团(存量码,本接口新可达):
|
||||
|
||||
```json
|
||||
{ "code": 589551, "message": "该团期已流团,不可下单", "success": false, "data": null }
|
||||
```
|
||||
|
||||
团期子订单人数变化但解析不到所属团期(存量码,本接口新可达,fail-closed):
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 鉴权、订单归属、窗口守卫(出行人页签 TRAVELLING 起锁、出行日期页签 PENDING_DEPARTURE 起锁)均未改。
|
||||
- 587045 在报价与任何写之前判定,拒绝即零写入、零报价调用。
|
||||
- 团期子订单人数变化的差价 = 团期报价(`getGroupQuote(班期, 成人, 儿童, 小童, 婴儿=0, 房数)`)终态 − 原态;正数记加收、负数记优惠;防超付守卫(587032)照常。只变婴儿数时不报价、差价为 0。
|
||||
- 团期已报名人数 `enrolled_people` 与出行人改动同事务:加人 +N(**不校验人数上限**)、减人 −N(不减到 0 以下)、只改资料或一增一减不变;报名房数 `enrolled_rooms` 不变。
|
||||
- 产品侧班期名额(C 端剩余人数)不随调整同步。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写后端接受/拒绝 payload 的规则。
|
||||
|
||||
| 场景 | payload | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 团期子订单加人 / 删人 | `updates.travelers.add` / `remove` | 200,差价按团期报价求差,已报名人数同步 |
|
||||
| ✅ 团期子订单回传未改动的出发日 | `updates.schedule.departDate` = 当前出发日 | 不拦,视为未改期 |
|
||||
| ✅ 团期子订单行程空对象 | `updates.itinerary = {}` | 不拦 |
|
||||
| ❌ 团期子订单改出发日 | `updates.schedule.departDate` ≠ 当前出发日(含团期出发日) | 587045,零写入 |
|
||||
| ❌ 团期子订单带行程 | `updates.itinerary.days` 非 null(哪怕与现状相同) | 587045,零写入 |
|
||||
| ✅ 散客单任意组合 | 同改前 | 行为不变 |
|
||||
|
||||
- 前端对团期子订单提交时**不要携带 `updates.itinerary.days`**,否则连出行人改动一起被拒。
|
||||
- 团期子订单改期请走团期转期,不走本接口。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
- 无 Flyway migration、无 DDL。
|
||||
- 团期子订单增删出行人:写 `order_traveler`、`order_main` 四档人数、差价(`order_surcharge` / `order_discount` 及主表镜像)、`order_adjustment_record`,与散客单同一套写法。
|
||||
- **新增写入**:同一事务内 `order_group_batch.enrolled_people` 按人数差 ±N(`enrolled_rooms` 不动);与创单 +N、取消 −N、对账 Job 同一人数公式(成人 + 儿童 + 小童 + 婴儿),偏差由 `GroupBatchEnrolledReconcileJob` 兜底。
|
||||
- 587045 / 581032 / 589551 / 589500 拒绝时整笔零写入。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 团期子订单改出发日 → 587045(取代 587039~587042)。
|
||||
- 团期子订单带 `itinerary.days` → 587045。
|
||||
- 团期子订单 `schedule` 同值回传、`itinerary` 空对象 → 不拦。
|
||||
- 团期子订单增删出行人 → 200(取代 587036)。
|
||||
- 团期报价失败 → 581032;团期已流团时加人 → 589551;解析不到团期 → 589500。
|
||||
- 散客单 → 行为不变。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
| 行为(团期子订单) | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 增删出行人 | 587036 拒绝 | 200,差价按团期报价求差 |
|
||||
| 已报名人数 `enrolled_people` | 调整不同步,等对账 Job | 同事务 ±N |
|
||||
| 改出发日为团期出发日 | 允许(#7325 出口) | 587045 |
|
||||
| 改出发日为其他日期 | 587039 | 587045 |
|
||||
| 提交行程 `itinerary.days` | 允许 | 587045 |
|
||||
| 错误码 587036、587039~587042 | 可能返回 | 不再返回(号段保留不复用) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 请求/响应结构不变;团期子订单原来能成功的「改回团期出发日」「提交行程」现在返回 587045,原来被拒的增删出行人现在成功。
|
||||
- **前端是否必须同步上线**: 建议同步。前端当前对出行人页签显示「团期子订单暂不支持调整出行人」,需去掉;出行日期、行程页签需对团期子订单置灰,且提交时不回传未改动的 `itinerary.days`,否则出行人改动会被 587045 连带拒掉。
|
||||
- **前端 workaround 清理点**: 前端针对 587036 / 587039~587042 的文案或分支可删除。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- 散客(非团期)订单的全部调整行为。
|
||||
- 团期子订单的酒店、车辆、接送机、紧急联系人子领域。
|
||||
- 团期转期、团期管理台其他接口。
|
||||
- 产品侧班期名额(`enrollBatch` / `unenrollBatch`)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关(TEST,`https://api.test.1814.love`,自签 admin token),造数:班期 `2103050046352322562`(团期报价 1/2/3 成人 = 3000/6000/9000)、团期 `2103050071622983681`、团期子订单 `HL20260924171304411` / `HL20260924171740384`、散客单 `HL20260924172017561`:
|
||||
|
||||
| 用例 | 请求 | 结果 | 库内读数 |
|
||||
|------|------|------|----------|
|
||||
| 团期子订单加 1 人 | `updates.travelers.add` | 200 | 成人 2→3;加收 3000(= 9000 − 6000);调整记录「总人数 2→3」;`enrolled_people` 2→3,`enrolled_rooms` 不变 |
|
||||
| 团期子订单只改资料 | `updates.travelers.update` | 200 | `enrolled_people` 不变 |
|
||||
| 团期子订单删 1 人 | `updates.travelers.remove` | 200 | 优惠 3000;`enrolled_people` 3→2 |
|
||||
| 已全额收款团期子订单删人 | `updates.travelers.remove` | 587032 | 零写入(防超付照常) |
|
||||
| 改出发日为团期出发日 / 其他日期 / 加人 + 改期 | `updates.schedule.departDate` | 587045 ×3 | 前后快照逐字相同,出行人未加 |
|
||||
| 行程原样回传 / 删一个节点 | `updates.itinerary.days` | 587045 ×2 | 零写入 |
|
||||
| 出发日同值回传(± 改资料) | `updates.schedule.departDate` = 当前值 | 200 | 只落资料修改,未改期 |
|
||||
| 散客对照:加人 / 改期 / 删人 / 改行程 | 同上 | 200 ×4 | 差价走个人报价(+8400 / −8400),改期未被 587045 拦 |
|
||||
|
||||
终态一致性:该团未取消子订单人数合计 4,等于 `enrolled_people=4`。
|
||||
|
||||
单元测试:`AdjustmentServiceSubmitTest` 嵌套类 `GroupSubOrderAdjustTests` 16 例等,定向集 `Tests run: 235, Failures: 0, Errors: 0`;`TransactionalRemoteCallArchTest` 绿。
|
||||
|
||||
部署: PR #8353 已合并 `dev-v3`(合并提交 `8df1c0e84`),TEST 双实例滚动部署完成。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8350](https://git.1814.love/wx/HL/issues/8350)
|
||||
- 关联 PR: [wx/HL#8353](https://git.1814.love/wx/HL/pulls/8353)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8350](https://git.1814.love/wx/HL/issues/8350)
|
||||
- **PR**: [#8353](https://git.1814.love/wx/HL/pulls/8353)
|
||||
- **Merge commit**: [8df1c0e84](https://git.1814.love/wx/HL/commit/8df1c0e84)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,433 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8354"
|
||||
title: "团期配导游 / 配摄影已配置人员列表化:名册姓名手机按人员 ID 实时查询(新增 staffStatus / liveInfoDegraded)+ 新增单人删除、单人更换接口"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "100e1b0a604e552c3b26f6789b323fbf453e69e4"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-26"
|
||||
status_note: "jw 2026-09-24 定:已配置人员改为列表展示(角色、姓名、手机号三列),做单人接口,姓名手机按人员 ID 实时查询,不动二期 #8258。PR #8357 已合 dev-v3(f457074ba)并部署 TEST,2026-09-24 17:54~18:00 经真实网关实测:名册接口把团期快照名「旧快照名」覆盖为资源库实时姓名;资源库已删除 / 已下架的人照常列出并分别标 NOT_FOUND / OFF_SHELF;对 order-v3 单个 Feign 客户端 staffBriefFeignClient 注入 1ms 超时(17:55:59~17:59:41,还原后 nacos 原文 SHA-256 逐字节一致)期间名册接口仍 200、全部行 liveInfoDegraded=true、值为快照;单人删除只删目标人,其他人的团期行与子订单副本 ID 不变;单人更换主报账人后报账人等级随行转移、预支候选变为新人;删除主报账人后 primaryReporterRemoved=true、预支候选为空;删光导游 / 摄影且有子订单需要该项时就绪位回落 0;不在本团 589508、新旧相同 100001、新人已在本团 589582、类型不符 582114、新人已下架 582118、缺 newStaffId 400、已确认团期 589598,均零写入;不带 token 401。前端需把「已配置人员」改为表格并接两个新接口,且须先修 24_frontend 配置弹窗误传团期 ID 的缺陷,故 frontend_status 记 pending。 前端已交付(100e1b0a):新建 ChipStaffRoster 名册表格(实时姓名手机+staffStatus 打标+degraded 提示+行内删除/更换接两新接口),面板换挂并传 productBatchId;589553 前置缺陷此前已闭环;spec 16 例全绿。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期人员配置:名册实时姓名手机 + 单人删除 / 更换(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8357
|
||||
> **Issue**: #8354
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 管理后台团期详情页「配导游」「配摄影」两个页签的「已配置人员」区域
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **名册接口的姓名、手机号改为实时值**。以前是保存配置时存下的快照,资源库改了名或换了手机也不会变;现在每次查询都按人员 ID 实时取资源库。新增两个字段:`staffStatus`(人员在资源库的状态)和 `liveInfoDegraded`(资源服务不可用时回落快照)。原有字段不变。
|
||||
2. **新增单人删除**:`DELETE /v3/admin/group-batch/{productBatchId}/staff/{staffId}`,只删这一个人,其他人不动,同步从各子订单上移除。
|
||||
3. **新增单人更换**:`PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/replace`,新人接替原人的角色位、排序、备注和报账人等级,同步替换到各子订单。
|
||||
4. 以上三个接口的路径变量都是**产品侧排期 ID**(`detail.productBatchId`),**不是**路由上的团期 ID。传错会拿到空名册或 589553,见 `24_frontend_团期详情配导游配摄影保存报589553弹窗误传团期ID-前端缺陷-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
「已配置人员」原来是一行文字(`领队 · 巴特尔 · 139****1011 领队 · 萨仁高娃 · 139****1008`),人多了看不清,也不能对单个人操作。jw 2026-09-24 定:改为表格,列为角色、姓名、手机号;做单人删除 / 更换接口;姓名手机按人员 ID 实时查询;继续使用 `order_batch_staff`,不动二期 #8258。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 查询团期人员名册 | GET | `/v3/admin/group-batch/{productBatchId}/staff` | 修改 | 姓名 / 手机改为实时值;新增 `staffStatus` / `liveInfoDegraded` |
|
||||
| 2 | 团期单人删除 | DELETE | `/v3/admin/group-batch/{productBatchId}/staff/{staffId}` | 新增 | 只删一人并同步子订单 |
|
||||
| 3 | 团期单人更换 | PUT | `/v3/admin/group-batch/{productBatchId}/staff/{staffId}/replace` | 新增 | 一人换一人并同步子订单 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 查询团期人员名册 `GET /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `List<BatchStaffConfigRespVO.BatchStaffItemVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
「配导游」「配摄影」页签打开时拉取已配置人员,前端按 `staffRole` 分到导游位(`GUIDE` / `LEADER`)和摄影位(`PHOTOGRAPHER`)两张表,展示角色、姓名、手机号。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 产品侧排期 ID | 取 `detail.productBatchId`,不是团期 ID |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Long(String) | 团期人员记录 ID(整位保存后会变,单人操作请用 `staffId`) |
|
||||
| staffId | Long | 人员 ID,单人删除 / 更换的路径参数 |
|
||||
| staffRole / staffRoleName | String | 角色及中文(领队 / 导游 / 摄影) |
|
||||
| staffName | String | **实时姓名**;`staffStatus` 不是 `ON_SHELF` 时为配置时快照 |
|
||||
| staffPhone | String | **实时手机号**,脱敏前 3 后 4;非 `ON_SHELF` 时为快照(可能为 null) |
|
||||
| avatarUrl | String | 头像(快照) |
|
||||
| sortOrder | Integer | 排序 |
|
||||
| remark | String | 备注 |
|
||||
| reporterRank / reporterRankName | String | 报账人等级及中文 |
|
||||
| staffStatus | String | **新增**。`ON_SHELF` 上架(实时值)/ `OFF_SHELF` 已下架 / `NOT_FOUND` 资源库查不到(已删除)/ `UNKNOWN` 资源服务不可用 |
|
||||
| liveInfoDegraded | Boolean | **新增**。`true` = 资源服务不可用,本行姓名手机为快照 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/group-batch/2101502082564407299/staff
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "2103044654230642690",
|
||||
"staffId": 1011,
|
||||
"staffRole": "LEADER",
|
||||
"staffRoleName": "领队",
|
||||
"staffName": "巴特尔",
|
||||
"staffPhone": "139****1011",
|
||||
"sortOrder": 1,
|
||||
"reporterRank": "NONE",
|
||||
"reporterRankName": "非报账人",
|
||||
"staffStatus": "ON_SHELF",
|
||||
"liveInfoDegraded": false
|
||||
},
|
||||
{
|
||||
"id": "8354000000001006",
|
||||
"staffId": 1006,
|
||||
"staffRole": "GUIDE",
|
||||
"staffRoleName": "导游",
|
||||
"staffName": "赵丽娜",
|
||||
"staffPhone": null,
|
||||
"sortOrder": 9,
|
||||
"reporterRank": "NONE",
|
||||
"reporterRankName": "非报账人",
|
||||
"staffStatus": "OFF_SHELF",
|
||||
"liveInfoDegraded": false
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
未配置人员返回 `[]`。资源服务不可用时仍返回 200,姓名手机为快照:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"staffId": 1011,
|
||||
"staffRole": "LEADER",
|
||||
"staffName": "巴特尔",
|
||||
"staffPhone": "139****1011",
|
||||
"staffStatus": "UNKNOWN",
|
||||
"liveInfoDegraded": true
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
判权未改动(`group-batch:view`)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 整团只调一次资源服务批量接口;资源服务异常、超时、返回失败时整体降级为快照。
|
||||
- 已下架、已删除的人**照常列出**,不会从名册里消失,前端可按 `staffStatus` 加提示(如「已下架」)。
|
||||
- 保存接口(`PUT .../staff`)的响应里这两个新字段为 `null`,只有本查询接口会填。
|
||||
|
||||
### 2. 团期单人删除 `DELETE /v3/admin/group-batch/{productBatchId}/staff/{staffId}`
|
||||
|
||||
**VO**: `BatchStaffRemoveRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
名册表格某一行点「删除」,只移除这一个人。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 产品侧排期 ID | 同名册接口 |
|
||||
| staffId | path | Long | 是 | 须在本团名册中 | 取名册行的 `staffId` |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| productBatchId | Long(String) | 回显 |
|
||||
| groupBatchId | Long(String) | 运营团期 ID |
|
||||
| removedStaffId | Long | 被删除的人员 ID |
|
||||
| removedStaffRole | String | 被删除人员的角色 |
|
||||
| primaryReporterRemoved | Boolean | 被删的是否是主报账人;`true` 时本团已无主报账人 |
|
||||
| affectedOrderCount | Integer | 同步删除副本的活跃子订单数 |
|
||||
| staffList | Array | 删除后的名册(快照值,需要实时值请重新调名册接口) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/group-batch/2103060031777988611/staff/1008
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"productBatchId": "2103060031777988611",
|
||||
"groupBatchId": "2103060033518661633",
|
||||
"removedStaffId": 1008,
|
||||
"removedStaffRole": "LEADER",
|
||||
"primaryReporterRemoved": false,
|
||||
"affectedOrderCount": 2,
|
||||
"staffList": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口,无空数据形态;校验失败零写入,返回下方错误码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| code | 触发条件 |
|
||||
|---|---|
|
||||
| `589508` | 此人不在本团(文案为「报账人不是该团期已派员工」,前端建议按码值自定义为「该人员不在本团」) |
|
||||
| `589598` | 团期已确认(物料准备中及以后)或已取消 |
|
||||
| `589553` | 团期未建团(通常是路径传成了团期 ID) |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589508,
|
||||
"message": "报账人不是该团期已派员工",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只删目标人,本团其他人的记录一行不动。
|
||||
- 同步删除团内各活跃子订单上此人的副本,并同步子订单导游 / 摄影状态。
|
||||
- 删光导游位或摄影位、且团内有子订单需要该项时,对应就绪位回落为未就绪。
|
||||
- `primaryReporterRemoved=true` 时建议提示「主报账人已移除,团期预支需重新设置主报账人」。
|
||||
|
||||
### 3. 团期单人更换 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/replace`
|
||||
|
||||
**VO**: `BatchStaffReplaceReqVO` → `BatchStaffReplaceRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
名册表格某一行点「更换」,从候选里选一个新人替换原人。候选用既有的 `GET .../staff/candidates/page?role=GUIDE|PHOTOGRAPHER`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productBatchId | path | Long | 是 | 产品侧排期 ID | 同名册接口 |
|
||||
| staffId | path | Long | 是 | 须在本团名册中 | 被换下的人 |
|
||||
| newStaffId | body | Long | 是 | 上架、类型符合原配置位、不在本团、不等于 staffId | 换上的人 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| productBatchId / groupBatchId | Long(String) | 回显 / 运营团期 ID |
|
||||
| oldStaffId / newStaffId | Long | 被换下 / 换上的人员 ID |
|
||||
| staffRole | String | 新人落库角色(导游位按新人真实类型存 `GUIDE` 或 `LEADER`) |
|
||||
| reporterRank | String | 新人继承的报账人等级 |
|
||||
| primaryReporterTransferred | Boolean | 被换下的是否是主报账人;`true` 时主报账人已转给新人 |
|
||||
| affectedOrderCount | Integer | 同步替换副本的活跃子订单数 |
|
||||
| staffList | Array | 更换后的名册(快照值) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"newStaffId": 1010
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"productBatchId": "2103060031777988611",
|
||||
"groupBatchId": "2103060033518661633",
|
||||
"oldStaffId": 1011,
|
||||
"newStaffId": 1010,
|
||||
"staffRole": "LEADER",
|
||||
"reporterRank": "PRIMARY",
|
||||
"primaryReporterTransferred": true,
|
||||
"affectedOrderCount": 2,
|
||||
"staffList": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
写接口,无空数据形态;校验失败零写入。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| code | 触发条件 |
|
||||
|---|---|
|
||||
| `400` | 缺 `newStaffId` |
|
||||
| `100001` | 新旧相同 |
|
||||
| `589508` | 原人不在本团 |
|
||||
| `589582` | 新人已在本团 |
|
||||
| `582114` | 新人类型不符合该配置位(如把摄影换进导游位) |
|
||||
| `582118` | **新增**。新人在资源库已下架 |
|
||||
| `582103` | 资源库取不到新人(文案为「请稍后重试」,可能是新人不存在) |
|
||||
| `589598` / `589553` | 团期已确认或已取消 / 未建团 |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582118,
|
||||
"message": "所选人员已下架,不能配置到团期,请重新选择",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 配置位不变:导游位只能换成导游或领队,摄影位只能换成摄影。
|
||||
- 新人继承原人的排序、备注、报账人等级;本团其他人一行不动。
|
||||
- 各活跃子订单上原人的副本换成新人,并同步子订单状态。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 三个接口的 `productBatchId` 一律取 `detail.productBatchId`。
|
||||
- 单人操作用名册行的 `staffId`,不要用 `id`(`id` 在整位保存后会变)。
|
||||
- 删除或更换成功后重新调名册接口刷新表格(响应里的 `staffList` 是快照值)。
|
||||
- 删除 / 更换主报账人后,团期预支的领款人候选会随之变化(删除后为空、更换后为新人)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 零表结构变更、零迁移脚本,继续读写 `order_batch_staff`。
|
||||
- 单人删除:软删此人团期行 + 软删本团活跃子订单上此人的 `order_staff_assignment`(`source=GROUP_BATCH`)副本。
|
||||
- 单人更换:软删原人团期行并插入新人一行;子订单上软删原人副本、插入新人副本。
|
||||
- 名册查询只读。
|
||||
- 所有校验失败零写入(TEST 实测写前写后比对一致)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 两个写接口与整位保存共用同一把团期锁,同一团期的写操作串行。
|
||||
- 已知窗口:若刚做过整位保存、其异步同步尚未完成就执行单人删除,被删的人可能在个别子订单上被那一轮同步写回;设置报账人等级有同类窗口。实际操作间隔秒级以上时不会出现。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 名册姓名 / 手机 | 保存配置时的快照 | 资源库实时值;不可用时回落快照 |
|
||||
| 名册新增字段 | 无 | `staffStatus`、`liveInfoDegraded` |
|
||||
| 删除一个人 | 把整个配置位的名单去掉此人后重新整体保存 | `DELETE .../staff/{staffId}` |
|
||||
| 换一个人 | 同上,整体保存 | `PUT .../staff/{staffId}/replace` |
|
||||
| 其他人的记录 | 每次整体保存都会重建 | 单人操作不动其他人 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- 前端:名册改表格、接两个新接口、按 `staffStatus` / `liveInfoDegraded` 加提示;原有字段不变,不改也不会报错。
|
||||
- 性能:名册查询多一次资源服务批量调用。
|
||||
- 出团通知书等内部读取名册的地方仍用快照,不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 整位保存 `PUT /v3/admin/group-batch/{productBatchId}/staff`、候选分页、设置报账人等级三个接口的契约。
|
||||
- 订单侧人员接口(`/v3/admin/order/{orderId}/staff*`)。
|
||||
- 二期 #8258 团期人员改表。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署 dev-v3 @ `f457074ba` 后,2026-09-24 17:54~18:00 经真实网关 `https://api.test.1814.love` 实测(自造团期 `2103060033518661633`,一户需要导游和摄影):
|
||||
|
||||
| 用例 | 期望 | 实测 |
|
||||
|---|---|---|
|
||||
| 构建身份:名册接口带 `staffStatus` | 新字段出现 | 4 次均带 |
|
||||
| 团期快照名改成「旧快照名」后查名册 | 返回资源库实时姓名 | 返回「萨仁高娃」 |
|
||||
| 资源库已删除 / 已下架的人 | 照常列出,`NOT_FOUND` / `OFF_SHELF` | 通过 |
|
||||
| 注入资源服务客户端 1ms 超时 | 200,`liveInfoDegraded=true`,快照值 | 通过;还原后 nacos 原文 SHA-256 一致,复核恢复 `ON_SHELF` |
|
||||
| 删除萨仁高娃 | 只删目标,其他人团期行 / 子订单副本 ID 不变 | 通过 |
|
||||
| 删除不在本团的人 | 589508 零写入 | 通过 |
|
||||
| 更换:新旧相同 / 原人不在本团 / 新人已在本团 / 类型不符 / 新人已下架 / 缺参 | 100001 / 589508 / 589582 / 582114 / 582118 / 400,零写入 | 通过 |
|
||||
| 更换主报账人 巴特尔 → 乌日娜 | 等级转移,预支候选变为乌日娜 | 通过 |
|
||||
| 删除主报账人 | `primaryReporterRemoved=true`,预支候选为空 | 通过 |
|
||||
| 删光摄影 / 导游(有户需要) | 对应就绪位回落 0 | 通过 |
|
||||
| 已确认团期删除 / 更换 | 589598 零写入 | 通过 |
|
||||
| 不带 token | 401 | 通过 |
|
||||
|
||||
造数已清理:订单取消、成团取消、产品侧班期取消。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8354、PR #8357
|
||||
- 前端缺陷:`24_frontend_团期详情配导游配摄影保存报589553弹窗误传团期ID-前端缺陷-管理后台.md`
|
||||
- 预支领款人限定主报账人:`24_8322_团期预支核单前均可发起与领款人限定主报账人-修改接口-管理后台.md`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,121 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "frontend"
|
||||
title: "团期详情「配置导游 / 配置摄影」保存必报 589553「团期尚未创建」:弹窗把订单侧团期 ID 当成产品侧排期 ID 传给人员配置接口"
|
||||
consumer: admin
|
||||
author: "jw(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c7059880ce6470e391c6ed3a9d6dfd5305ed037a"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "2026-09-24 jw 在 TEST 团期详情(/order-v2/batch/detail/2101506167098511362,jw测试产品 第1期,已成团 12 户)点「配置导游」勾选两人保存,弹出「团期尚未创建(该班期还没有任何订单),请先建团并完成成团后再操作」(589553)。根因在 hl-ui:团期详情页把路由上的订单侧团期 ID(groupBatchId)传给 GroupBatchStaffConfigModal,弹窗再原样拼进 GET / PUT /v3/admin/group-batch/{productBatchId}/staff 与候选分页接口,而这组接口的路径变量是产品侧排期 ID。后端按产品侧排期 ID 反查团期,拿团期 ID 当排期 ID 查不到,按设计抛 589553。同页「设置主报账人」弹窗传的是 productBatchId,是对的。后端与数据均正常:该团期 order_group_batch 行存在(group_batch_id=2101506167098511362 / product_batch_id=2101502082564407299 / RESOURCE_PREPARING / 子订单 14 张);order_batch_staff 中按团期 ID 错存的活跃行 0 条,无需修数。本条无后端改动。 | 2026-09-24 mmg 修复:弹窗 prop 改传 detail.productBatchId+空 ID 闸;prop 与三 API 形参统一改名 productBatchId,JSDoc 补 589553 误传口径;index spec 新增 2 例回归(prop=detail.productBatchId 非路由 ID/空 ID 不开弹窗),弹窗 spec 6 例+index spec 5 例全绿"
|
||||
updated_at: "2026-09-24"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 团期详情「配置导游 / 配置摄影」保存报 589553:弹窗传错了 ID
|
||||
|
||||
> **服务**: hl-order-service-v3(**后端零改动,后端与数据均正常**)
|
||||
> **页面**: 管理后台 → 团期订单 → 团期详情 → 「配导游」/「配摄影」Tab → 「配置导游」/「配置摄影」弹窗
|
||||
> **接口**: `GET` / `PUT /v3/admin/group-batch/{productBatchId}/staff`、`GET /v3/admin/group-batch/{productBatchId}/staff/candidates/page`
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 仅前端。所有团期、所有用户,配置导游 / 配置摄影一保存就失败;人员配置实际一直存不进去
|
||||
|
||||
---
|
||||
|
||||
## 一、现象
|
||||
|
||||
团期已成团,打开「配置导游」,候选列表正常显示,勾选人员点「保存」,右上角报错:
|
||||
|
||||
`团期尚未创建(该班期还没有任何订单),请先建团并完成成团后再操作`
|
||||
|
||||
「已配置人员」一直显示「暂未配置」。同一页的「设置主报账人」正常。
|
||||
|
||||
---
|
||||
|
||||
## 二、根因(对 hl-ui `origin/v2.1` = `9eb8268f` 源码实读)
|
||||
|
||||
**① 详情页传给弹窗的是订单侧团期 ID**
|
||||
|
||||
`src/views/order-v2/batch/detail/index.vue`:
|
||||
|
||||
```vue
|
||||
<!-- :258-263 -->
|
||||
<GroupBatchStaffConfigModal
|
||||
v-model:show="staffConfigShow"
|
||||
:group-batch-id="groupBatchId"
|
||||
:role="staffConfigRole"
|
||||
@saved="onStaffConfigSaved"
|
||||
/>
|
||||
```
|
||||
|
||||
`groupBatchId`(`:454`)取自路由,就是地址栏里的订单侧运营团期 ID(例:`2101506167098511362`)。
|
||||
|
||||
**② 弹窗把它拼进三个接口的路径**
|
||||
|
||||
`src/views/order-v2/batch/detail/components/GroupBatchStaffConfigModal.vue`:
|
||||
|
||||
| 行 | 调用 |
|
||||
|---|---|
|
||||
| `:182` | `getGroupBatchStaff(props.groupBatchId, …)` → `GET /v3/admin/group-batch/{id}/staff` |
|
||||
| `:197` | `getGroupBatchStaffCandidatesPage(props.groupBatchId, …)` → `GET …/{id}/staff/candidates/page` |
|
||||
| `:274` | `saveGroupBatchStaff(props.groupBatchId, …)` → `PUT /v3/admin/group-batch/{id}/staff` |
|
||||
|
||||
**③ 这组接口的路径变量是产品侧排期 ID**
|
||||
|
||||
后端 `GroupBatchStaffAdminController` 四个端点的路径变量都是 `{productBatchId}`(`group_tour_batch.batch_id`)。
|
||||
前端自己的 API 封装 `src/api/orderV2GroupBatch.js` 在 `getGroupBatchStaff` 注释里也写明
|
||||
「`@param groupBatchId 产品侧班期 ID(group_tour_batch.batch_id)`」——**只是参数名起成了 groupBatchId,调用方就按名字传了团期 ID**。
|
||||
|
||||
**④ 为什么只有保存报错**
|
||||
|
||||
- 查询已配置人员(GET staff)按排期 ID 查人员表,传错 ID 只是查到空列表,不报错 → 页面显示「暂未配置」。
|
||||
- 候选分页不校验团期存在性,候选照常出来;但它的返回里已经露了馅:每条记录的 `productBatchId` 回显成了团期 ID、`groupBatchId` 为 `null`(后端认为「这个排期还没建团」)。
|
||||
- 保存(PUT staff)先按排期 ID 反查团期、校验可配置窗口;拿团期 ID 当排期 ID 查不到团期行,按设计返 `589553`。
|
||||
|
||||
**对照**:同页「设置主报账人」弹窗(`index.vue:268-273`)传的是 `:product-batch-id="productBatchId"`(`:756`,取 `detail.productBatchId`),所以它是好的。
|
||||
|
||||
---
|
||||
|
||||
## 三、后端与数据侧的核查结果(均正常,供排除用)
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 团期是否存在 | `order_group_batch`:`group_batch_id=2101506167098511362`、`product_batch_id=2101502082564407299`、`batch_status=RESOURCE_PREPARING`,子订单 14 张 |
|
||||
| 按错误 ID 反查 | `product_batch_id=2101506167098511362` 的团期行不存在 → 589553 是正确的后端行为 |
|
||||
| 是否有错存数据 | `order_batch_staff` 中 `product_batch_id` 等于某个团期 ID 的活跃行:**0 条**,无需修数 |
|
||||
| 两个 ID 调 GET staff | 均返回 `200` + `[]`(GET 不校验团期存在性,所以传错也不报错) |
|
||||
| 用团期 ID 调候选分页 | `200`,`total=7`,记录里 `productBatchId=2101506167098511362`、`groupBatchId=null` |
|
||||
|
||||
---
|
||||
|
||||
## 四、修复建议
|
||||
|
||||
**最小改动**(一行):`index.vue:260` 改为传产品侧排期 ID。
|
||||
|
||||
```vue
|
||||
<GroupBatchStaffConfigModal
|
||||
v-model:show="staffConfigShow"
|
||||
:group-batch-id="productBatchId"
|
||||
:role="staffConfigRole"
|
||||
@saved="onStaffConfigSaved"
|
||||
/>
|
||||
```
|
||||
|
||||
弹窗内部读取、候选、保存三处都走这个 prop,会一起变对。
|
||||
|
||||
**建议一并做**:把弹窗 prop 与三个 API 封装的参数名改成 `productBatchId`(与 `ReporterRankModal` / `setGroupBatchStaffReporterRank` 一致),避免下一个调用方再按名字传错。
|
||||
|
||||
**建议补的回归**:`GroupBatchStaffConfigModal.spec.js` / `index.spec.js` 断言保存请求 URL 里的 ID 等于 `detail.productBatchId`,而不是路由 ID——现有弹窗用例只给了一个 `groupBatchId: '2099751575655247875'`,没有同时构造两种 ID,所以传错也测不出来。
|
||||
|
||||
---
|
||||
|
||||
## 五、业务边界
|
||||
|
||||
- 修复后可配窗口按后端现行规则:招募中、资源准备中可配,确认(物料准备中)之后返 `589598`(#8231 / #8269)。
|
||||
- `detail.productBatchId` 为空时(极端情况)应禁用「配置导游 / 配置摄影」按钮,不要拼出 `/group-batch//staff`。
|
||||
- 本条不涉及后端改动,接口契约不变。
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "团期详情「配房 / 配车」明细的“更新时间”列恒为空,建议隐藏该列或补真实时间源"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "6e6fe48982e6b39f01aba5f5c1236437f999b7de"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "浏览器实测(测试服 web.test.1814.love,2026-09-24 17:0x,团期 2102981652823302146,4 户):团期详情 → 配房 / 配车 两个芯片明细列表的“更新时间”列四行全部为「—」。对照后端契约:GET /v3/admin/order/group-batch/{groupBatchId}/chips/hotel 与 /chips/vehicle 的 items[].updateTime 恒为 null;后端源码 GroupBatchChipResolver#toItem 明确注释“无状态时间列时 updateTime=null”,ChipItem.updateTime 字段 javadoc 同款(“六状态列无独立时间戳”)。该列对房/车两块芯片永远渲染不出值,属纯前端展示问题,无需后端改动。 前端已交付并验证:ChipItemsTable 更新时间列改数据驱动显隐——全行 updateTime 为 null(房/车契约恒 null)整列不渲染,任一行有真值(约/保)出列截到分钟直显;不读 chipLabel 判芯片,禁拿 status-logs 凑值;列表弹层 ChipDetailModal 共用同组件自动生效。ChipStaffDisplay spec 7 例全绿(新增 2 例),hl-admin@6e6fe489。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期详情 · 配房/配车明细「更新时间」列恒为空
|
||||
|
||||
> **端**: 管理后台 | **页面**: 团期详情 → 配房 / 配车 | **Issue**: 无(前端展示优化,已核实无需后端改动)
|
||||
|
||||
---
|
||||
|
||||
## 一、现象(浏览器实测)
|
||||
|
||||
测试服 `https://web.test.1814.love` → 团期详情 `2102981652823302146`(4 户):
|
||||
|
||||
- **配房** tab:表头为 `联系人 | 订单号 | 人数 | 状态 | 更新时间`,四行(甲/乙/丙/丁)的「更新时间」**全部显示为「—」**;
|
||||
- **配车** tab:同一表格结构,四行「更新时间」**同样全部为「—」**(含状态已经是「配房完成」「配车完成」的两户)。
|
||||
|
||||
截图见本次浏览器测试留档。
|
||||
|
||||
## 二、根因(后端契约已核实,不是偶发渲染)
|
||||
|
||||
两个芯片明细接口对每一户返回的 `updateTime` **恒为 null**:
|
||||
|
||||
```json
|
||||
{ "orderId": "2102981652680695809", "teamNo": "26-6045",
|
||||
"status": "PENDING", "statusText": "待车务配", "updateTime": null }
|
||||
```
|
||||
|
||||
后端源码(hl-order-service-v3)两处写死这一口径:
|
||||
|
||||
- `GroupBatchChipResolver#toItem` 注释:「单户逐户派生(status 按芯片口径,**无状态时间列时 updateTime=null**)」,构造实参直接传 null;
|
||||
- `GroupBatchChipResolver.ChipItem.updateTime` 字段 javadoc:「该项最后变更时间;**无独立来源列时为 null(六状态列无独立时间戳)**」。
|
||||
|
||||
也就是说:房/车两块芯片的状态取自 `order_main.room_control_status / vehicle_control_status` 等**六状态列**,这些列没有配套的时间戳列,所以该字段在契约上永远是 null。**要让它有值必须后端补数据源(新增列或改用状态流转日志),不在本条目范围。**
|
||||
|
||||
## 三、建议(前端)
|
||||
|
||||
两条路都不需要后端配合,推荐第一条:
|
||||
|
||||
1. **(推荐)对房/车两块芯片隐藏「更新时间」列**:若该表格组件同时服务约/保等芯片,可按 `chipLabel` 动态隐藏,或按「本页所有行 updateTime 皆为 null 时不渲染该列」,避免一整列永远只有「—」的占位列;
|
||||
2. 若产品要求保留该列,则应先由后端补时间源;在当前契约下保留该列只会持续展示空值。
|
||||
|
||||
配套约束:**不要**在前端用 `status-logs` 等接口的最近一条时间「近似凑值」——那会让同一列在不同芯片上口径不一致(约/保是真值、房/车是近似值),排查成本比留空更高。
|
||||
|
||||
## 四、影响范围
|
||||
|
||||
- **仅展示**:团期详情 → 配房 / 配车 两个 tab 的明细表格列。
|
||||
- **零影响**:接口契约与字段结构(`updateTime` 仍按契约返回 null)、聚合态与完成计数、房务/车务的实际操作链路。
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "frontend"
|
||||
title: "「查看需求」Tab 缺失清单改显团号、需求状态列漏看车侧缺失"
|
||||
consumer: admin
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "8e2f6acd26f5124adf16787df0e61a5a2d8e0d5f"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "2026-09-24 发现两处前端缺陷,均不涉及后端改动。①顶部缺失清单用订单号标识户,应改为团号(团号已由接口 GroupBatchOrderItemRespVO.teamNo 提供);②需求状态列只判住宿缺失,未考虑车侧缺失导致「住宿不缺但车侧缺」的户显示「已齐备」不符实。两项改动的数据来源均已在现有接口提供,后端零改动。 前端 2026-09-24 闭环 verified:①三处缺失清单改按 orderId 查名单行 teamNo 显团号,teamNo null(未付订金)/查不到回落订单号,整团级条目不加户标识;②需求状态列判已齐备须住宿 missing 与 vehicleMissing 双无本户条目,车侧命中显红色缺失标签(文案 detail),整团级 orderId 空条目不归户;整团确认仍直读 ready(hl-admin 8e2f6acd)。RequirementTab.spec 30 例全绿(含团号回落/车侧状态列 4 新例),checkpoint 全过。"
|
||||
updated_at: "2026-09-24"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 「查看需求」Tab:缺失清单用订单号改为团号、需求状态列漏看车侧缺失
|
||||
|
||||
> **页面**: 管理后台 → 团期详情 → 「查看需求」Tab
|
||||
> **文件**: `src/views/order-v2/batch/detail/components/RequirementTab.vue`(hl-ui `v2.1` 分支,读的是 commit `04e08d1a9ddb`)
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 前端。所有团期所有用户
|
||||
|
||||
---
|
||||
|
||||
## 一、问题 1:顶部缺失清单用订单号标识户,应改为团号
|
||||
|
||||
### 现象
|
||||
|
||||
顶部「仍有子订单需求缺失,暂不能整团确认」提示框内,三处缺失列表用订单号标识每户:
|
||||
|
||||
| 列表 | 渲染字段 | 代码行 |
|
||||
|---|---|---|
|
||||
| 住宿缺失 | `missingItems` 渲染 `m.orderNo \|\| String(m.orderId)` | 约第 102 行 |
|
||||
| 车侧缺失 | `vehicleMissingItems` 渲染 `v.orderNo` | 约第 114-117 行 |
|
||||
| 接送机缺口 | `transferGapItems` 渲染 `t.orderNo \|\| String(t.orderId)` | 约第 138 行 |
|
||||
|
||||
实例:测试服团期「第3期 10月8日出发团」,户「王二麻子」在这些列表显示为 `HL20260922161158291`(订单号),但下方名单表同一户显示为 `26-2355`(团号)。
|
||||
|
||||
### 修法
|
||||
|
||||
统一改为显示**团号**,与下方名单表一致。
|
||||
|
||||
数据来源:同页名单表每行(接口 `GroupBatchOrderItemRespVO`)已带 `teamNo` 字段(团号,来自 `order_main.team_no`;**订金支付成功后生成,未付订金为 null**)。按 `orderId` 在名单行中查找对应 `teamNo`。
|
||||
|
||||
**兜底规则**:
|
||||
- `teamNo` 为 null(未付订金的户)或名单里查不到该 orderId 时,回落现有显示(订单号)
|
||||
- 整团级车侧条目(`orderNo`/`orderId` 为空,例「团期「第3期 10月8日出发团」尚未形成正式用车需求」)保持现状,不加户标识
|
||||
|
||||
### 验证
|
||||
|
||||
现有接口数据已足够,后端零改动。
|
||||
|
||||
---
|
||||
|
||||
## 二、问题 2:需求状态列只看住宿缺失,车侧缺失的户显示「已齐备」
|
||||
|
||||
### 现象
|
||||
|
||||
名单表「需求状态」列(`key: 'requirementStatus'`,约第 854-876 行)自相矛盾:
|
||||
|
||||
实例:同一团期户「王二麻子」(团号 26-2355)
|
||||
- 「需求审核」列显示「车 未提交」
|
||||
- 顶部提示框列出「该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务」(后端码 809122)
|
||||
- 但「需求状态」列却显示绿色「已齐备」
|
||||
|
||||
### 根因
|
||||
|
||||
「需求状态」列的判定逻辑只查 `missingMap`(由住宿侧 `checkResult.missing` 建立,约第 469 行),而 `checkResult.vehicleMissing` 完全未参与。
|
||||
于是「住宿不缺但车侧有缺」的户落到「预检已加载且不在缺失清单」分支,错误显示「已齐备」。
|
||||
|
||||
### 修法
|
||||
|
||||
判「已齐备」必须同时满足:
|
||||
1. 不在住宿 `missing` 里(现有逻辑)
|
||||
2. `vehicleMissing` 里没有 `orderId` 等于本户的条目(新增,字符串比较)
|
||||
|
||||
**显示规则**:
|
||||
- 车侧命中时显示红色缺失标签,文案用该条目的 `detail` 字段(后端返回的人话,与整团确认时报错逐字相同,可直接展示)
|
||||
- 同一户车侧可能有多条条目(后端「全部列出、不按户合并」),显示第一条即可,或自行决定合并方式
|
||||
- 住宿与车侧都命中时两类缺失都要能看出来
|
||||
- 整团级车侧条目(`orderId` 为空)不归到任何一户,不影响每户这一列(它已在顶部提示框单独展示)
|
||||
|
||||
### 后端契约
|
||||
|
||||
`GroupBatchRequirementCheckRespVO.VehicleMissingItem` 各字段:
|
||||
- `reason`:缺失原因编码(本例为 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`)
|
||||
- `groupCode`:涉及的乘车分组编码(如 `BUS`),无分组维度时为 null。**注意它不是团号**,团号只在名单行的 `teamNo`
|
||||
- `tripDate`:涉及的日期,无日期维度时为 null
|
||||
- `orderId`:涉及的子订单 ID,为空表示整团级条目
|
||||
- `orderNo`:子订单号快照
|
||||
- `detail`:人话描述,与整团确认时报错逐字相同,可直接展示
|
||||
|
||||
后端零改动。
|
||||
|
||||
### 测试覆盖
|
||||
|
||||
`__tests__/RequirementTab.spec.js` 现有测试(第 139-140 行、529-530 行)只喂了住宿侧数据,修后建议补一条用例:「住宿不缺但车侧有本户条目 → 不显示已齐备」。
|
||||
|
||||
### 验证
|
||||
|
||||
现有接口数据已足够,后端零改动。
|
||||
|
||||
---
|
||||
|
||||
## 三、业务边界
|
||||
|
||||
- **两项均不依赖后端改动**。修法所需数据已由现有接口提供(名单表 `teamNo`、`vehicleMissing` 字段)
|
||||
- **问题 1**:团号为 null(该户未付订金)时回落订单号,所以同一提示框里可能同时出现团号和订单号,属预期
|
||||
- **问题 2**:只影响「需求状态」列的显示;「整团确认需求」按钮是否可点仍以接口 `ready` 为准,前端不要自己重算
|
||||
- 后端有三条错误报文(809121 / 809112 / 809114)的 `msg` 里仍拼着订单号或雪花 ID,已另开后端工单 wx/HL#8306 改成团号;前端直接展示 `msg`,不需要为此改代码
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
schema: hl-changelog/v2
|
||||
ticket: "frontend"
|
||||
title: "核心订单首次排车:统一选择车辆/司机弹窗候选全空,报「canonicalSnapshot 缺少完整的代际、稳定派车组或服务日期」"
|
||||
consumer: admin
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端缺陷"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "75c053035247e577f2646610c93abc7ee6e641bc"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "2026-09-24 同事报「核心订单的车务排车不对」,定位为前端缺陷:尚未产生任何派车行的订单(看板「待派车」)第一次打开「统一选择车辆/司机」时,GET 候选接口返回的 canonicalSnapshot.retainedGroupIds 是空数组(#7067 去槽位化后首次排车本就没有派车组),而前端 normalizeCanonicalAssignmentSnapshot 把空数组判为快照非法直接 throw,整条候选响应作废——车辆候选、司机候选都拿不到,弹窗显示红色报错与「没有符合筛选条件的司机」。后端接口本身正常(车辆/司机候选都返回了,用同一份响应喂 dailyPlan 提交后四步链可跑通),需要前端放宽校验。 前端已修复并验证:normalizeCanonicalAssignmentSnapshot 撤掉 retainedGroupIds 非空校验(槽位时代遗留),空组+空格合法放行、空组带格仍判越界、非空组缺格仍判矩阵不完整;canonical-assignment-snapshot spec 17 例全绿(新增 2 例),hl-admin@75c05303。"
|
||||
updated_at: "2026-09-24"
|
||||
base: dev-v3
|
||||
---
|
||||
|
||||
# 车务派单看板:核心订单首次排车「统一选择车辆/司机」候选全空(canonicalSnapshot 空派车组被前端判非法)
|
||||
|
||||
> **页面**: 管理后台 → 车务 → 派单看板 →「统一选择车辆/司机」弹窗(四步向导第②步)
|
||||
> **文件**: `src/views/fleet/board/utils/canonical-assignment-snapshot.js`(`normalizeCanonicalAssignmentSnapshot`,hl-ui `v2.1`;测试服部署产物 `assets/AssignModal-*.js` 内函数 `yt`)
|
||||
> **日期**: 2026-09-24
|
||||
> **影响范围**: 前端。所有「当前用车需求下还没有任何派车行」的订单——即看板「待派车」的全部订单(本次实测的 26-0013 就是这种),团期子订单同一代码路径同样命中
|
||||
|
||||
---
|
||||
|
||||
## 一、现象
|
||||
|
||||
同事报「核心订单的车务排车不对」,截图里弹窗顶部是红字:
|
||||
|
||||
```text
|
||||
canonicalSnapshot 缺少完整的代际、稳定派车组或服务日期
|
||||
```
|
||||
|
||||
且弹窗里 **车辆候选和司机候选都是空的**(右侧显示「没有符合筛选条件的司机」),车务因此完全无法为该订单选车排车。
|
||||
|
||||
实例:订单 `HL20260924152729671`(团号 `26-0013`,客户「谢谢」,2026-09-30 ~ 2026-10-03,看板状态「待派车」)。
|
||||
|
||||
## 二、复现与证据(测试服实测,只读)
|
||||
|
||||
1. 车务角色登录 → `GET /admin/fleet/board/orders/2103023501973848066`:`requirementId=2103024417066123265`、`vehicleSlots=[]`、`activeAssignments=[]`(该单尚未派车);
|
||||
2. `POST /admin/fleet/assignments/candidates`(前端同形请求体)→ **HTTP 200 / code 200**,`data.canonicalSnapshot`:
|
||||
|
||||
```json
|
||||
{
|
||||
"planGeneration": "361414334036971520",
|
||||
"snapshotVersion": 1,
|
||||
"retainedGroupIds": [],
|
||||
"editableServiceDates": ["2026-09-30", "2026-10-01", "2026-10-02", "2026-10-03"],
|
||||
"cells": [],
|
||||
"selectedVehicle": null,
|
||||
"selectedDriver": null
|
||||
}
|
||||
```
|
||||
|
||||
同一份响应里 `data.vehicles.records` 与 `data.drivers.records` **都是有数据的**(车辆候选带车牌/车型/协议价,司机候选带姓名/脱敏手机号)——即后端没有出错,是前端在校验阶段把整份响应丢掉了。
|
||||
|
||||
3. 结论:`normalizeCanonicalAssignmentSnapshot` 的第一个校验分支要求 `retainedGroupIds` **非空**,空数组直接 throw,异常冒泡到候选请求的 catch,于是 `canonicalSnapshot = null` 之后的赋值全部没执行、`apiVehicles/apiDrivers` 保持空 → 弹窗只剩报错 + 空候选。
|
||||
|
||||
> 证据文件:`D:/work2/_task-evidence/fleet-core-dispatch/evidence/02-candidates-travel-26-0013.json`(26-0013 的真实响应)
|
||||
> 对照:同一个接口在订单**已经有派车行**之后返回 `retainedGroupIds=["361428998049370112"]` + 3 个 cells,前端校验通过、弹窗正常。
|
||||
|
||||
## 三、根因
|
||||
|
||||
后端 `retainedGroupIds` 的语义是 **RetainedGroupSet:当前需求下已存在的稳定派车组 ID**:
|
||||
|
||||
- #7067「派单去槽位化」之前,订单进入排车时后端会自动建 `unassigned` 占位槽位行,所以快照里**至少有一个槽位**,前端「非空」这条前置条件一直成立;
|
||||
- #7067 之后**不再建占位行**,派车行只由第②步「逐日方案提交」产生(`Step2CanonicalSnapshotService` 类注释:「首次进入且无存活行时 retained 为空集」)。于是**第一次排车**时 `retainedGroupIds=[]`、`cells=[]` 是**完全合法的状态**,表示「这一版方案还没有任何派车组」,栅格应为空、由车务现场选车生成。
|
||||
|
||||
前端的校验仍是槽位时代写的「必须非空」,故把合法空态误判为快照非法。
|
||||
|
||||
## 四、修法(前端)
|
||||
|
||||
`normalizeCanonicalAssignmentSnapshot` 需要接受「空派车组」这一合法状态:
|
||||
|
||||
- `planGeneration`、`snapshotVersion` 缺失/为空 → 仍按现在处理(快照不可用);
|
||||
- `retainedGroupIds` 为**空数组**且 `cells` 为**空数组** → 返回合法快照 `{planGeneration, snapshotVersion, retainedGroupIds: [], editableServiceDates, cells: [], selectedVehicle, selectedDriver}`,**不要 throw**;
|
||||
- 只有「`retainedGroupIds` 非空但 `cells.length != groups × dates`」「cells 越界/重复」这类**不一致**才继续判非法;
|
||||
- `editableServiceDates` 为空的判断保持不变(没有服务日确实无法排车)。
|
||||
|
||||
下游要注意的只有一处:空 `retainedGroupIds` 时 `reconcileCanonicalAssignmentDraft` 会算出「0 张卡片」,这正是期望结果——车务在弹窗里选车/司机 → 应用后走本地新草稿(`isNewDraft`)→ 第②步提交照旧走稀疏 `dailyPlan`(提交体只带 `serviceDate+vehicleId+driverId`,与派车组无关)。**不要把空数组退化成「快照不可用 → `canonicalSnapshot=null` 的旧模式」**,去槽位化后旧模式依赖的槽位数据已不再下发,会得到另一个空栅格。
|
||||
|
||||
## 五、请前端确认/回归的点
|
||||
|
||||
1. 打开任意「待派车」订单(如测试服 26-0013)→「统一选择车辆/司机」:不再出现红字报错,车辆/司机候选正常列出;
|
||||
2. 在该弹窗选车+选司机 → 应用 → 栅格出现一张待提交卡片 → 提交方案成功(后端 `POST /admin/fleet/assignments/batch` 返回 200);
|
||||
3. 已派车订单再打开同一弹窗:行为与现在一致(栅格回显已有派车组的日期格);
|
||||
4. 建议补一条单测:`canonicalSnapshot.retainedGroupIds=[]` + `cells=[]` → 归一化通过且 `retainedGroupIds` 为 `[]`(现有用例全是非空快照,所以这条错法在测试里看不见)。
|
||||
|
||||
## 六、后端改动
|
||||
|
||||
无(`backend_status: not_required`)。后端候选接口返回的数据本身正确,本次只需前端放宽校验。
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "订单列表撤掉团期ID精确筛输入框"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "45d946d615082e4c6fa3221b1bbc48fa2bb89cb9"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-24"
|
||||
status_note: "纯前端条目,后端零改动。按 wx 2026-09-24 要求撤掉订单列表(/order-v2/list)筛选区的「团期 ID」精确筛输入框,团期筛选统一走 #8252 交付的「团期」关键词框(团号/团期名/期次)。groupBatchId 入参后端保留不动(#7635 冻结契约,其它页面/链接仍有依赖),前端其它调用点也不得删除。 前端 2026-09-24 闭环 verified:筛选区「团期 ID」form-item/searchFields 项/fetchGroupCounts 透传 3 处全删并清理死引用注释,groupBatchKeyword(团号/团期名/期次)成唯一团期筛选入口;groupBatchId 入参契约保留,列表与六页签计数不再携带(hl-admin 45d946d6)。checkpoint 全过,列表 __tests__ 实证无引用。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单列表撤掉团期 ID 精确筛输入框(前端优化)
|
||||
|
||||
> **页面**: 管理后台「订单管理v2 → 订单列表」 `/order-v2/list`
|
||||
> **文件**: `src/views/order-v2/list/index.vue`
|
||||
> **代码基线**: `origin/v2.1`(2026-09-24 取,该文件最新改动 5f27c009)
|
||||
> **日期**: 2026-09-24
|
||||
> **后端**: 零改动,`GET /v3/admin/order` 出入参不变
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
团期筛选收敛为**一个入口**:按团号 / 团期名 / 期次检索的「团期」关键词框(#8252 已交付)。
|
||||
**撤掉「团期 ID」精确筛输入框**——不再要求定制师/运营手输 19 位雪花运营团期 ID。
|
||||
|
||||
---
|
||||
|
||||
## 一、调整清单
|
||||
|
||||
### 1.1 删除筛选区的「团期 ID」输入框
|
||||
|
||||
`src/views/order-v2/list/index.vue:65-73` 现状:`label` 为「团期 ID」,placeholder 为 `运营团期 ID(强制团单)`,
|
||||
`v-model:value="searchForm.groupBatchId"`。
|
||||
|
||||
> 文案提示:早期构建的 placeholder 是「运营团期 ID(定制团期)」(截图即该版本),现已改为「强制团单」。
|
||||
> **请按 `searchForm.groupBatchId` 绑定定位,不要按文案找。**
|
||||
|
||||
三处一起收拾干净,避免留下死字段:
|
||||
|
||||
| # | 位置 | 现状 | 改法 |
|
||||
|---|---|---|---|
|
||||
| 1 | `index.vue:65-73` | 团期 ID 的 `form-item`(含 `n-input`) | 整块删除 |
|
||||
| 2 | `index.vue:292` | `searchFields` 中的 `{ key: 'groupBatchId', default: '' }` | 删除该行 |
|
||||
| 3 | `index.vue:336` | `fetchGroupCounts()` 中 `if (searchForm.groupBatchId) params.groupBatchId = ...` | 删除该行(页签计数与列表同口径) |
|
||||
|
||||
- **列表请求侧**:`groupBatchId` 只经 `searchFields` 透传(同文件 `:244-245` 注释),删掉第 2 处后列表请求自然不再携带该参数。
|
||||
- **无 URL 依赖**:本页 `syncUrl: false`(`:296`),筛选不入 URL、也不消费 URL 入参,删除不影响任何跳转或刷新恢复。
|
||||
- **无测试引用**:列表模块内除 `index.vue` 外没有第二处引用(含 `src/views/order-v2/list/__tests__/**`),无需改测试。
|
||||
|
||||
### 1.2 保留不动(禁止顺手删)
|
||||
|
||||
| 保留项 | 位置 | 原因 |
|
||||
|---|---|---|
|
||||
| 「团期」关键词框 `groupBatchKeyword` | `index.vue:77-86`、`:294`、`:338` | #8252 交付的团期筛选正主(团号/团期名/期次,`maxlength 64`),本次唯一保留的团期筛选入口 |
|
||||
| 「订单类型」`orderKind`(NORMAL/GROUP/ALL) | `index.vue:55-64`、`:291`、`:335` | #7635 的团属性筛选仍然需要;撤掉 ID 框后「只看团单」走这里,不再依赖「输 ID 强制 GROUP」 |
|
||||
| 行级团期展示字段 `groupBatchName` / `groupBatchLabel` | `index.vue:212-215` | #8252「第N期 团期名」列展示,与筛选无关 |
|
||||
|
||||
### 1.3 接口契约不变(`groupBatchId` 后端保留)
|
||||
|
||||
`GET /v3/admin/order`(别名 `/v3/admin/order/list`)与 `GET /v3/admin/order/status-group-counts` 的
|
||||
可选入参 `groupBatchId` **照旧可用**,本次只是管理后台不再露出输入入口:
|
||||
|
||||
- #8252 定案第 2 条原文:「是否改动既有 `groupBatchId` 入参 → **保留不动,只新增 `groupBatchKeyword`**」;
|
||||
依据是它属 #7635 冻结契约、有专门错误码兜底,且订单详情/团期看板侧仍有依赖。
|
||||
- 因此**其他页面/调用点不得同步删除** `groupBatchId` 的调用能力,本页只是不再提供输入框。
|
||||
|
||||
---
|
||||
|
||||
## 二、验收
|
||||
|
||||
- [ ] `/order-v2/list` 筛选区不再出现「团期 ID / 运营团期 ID」输入框
|
||||
- [ ] 列表请求(`getOrderPage`)与页签计数请求(`getOrderStatusGroupCounts`)都不再携带 `groupBatchId`
|
||||
- [ ] 「团期」关键词框仍按团号/团期名/期次检索,列表与六个页签计数同口径
|
||||
- [ ] 「订单类型」NORMAL/GROUP/ALL 切换后列表与页签计数同步刷新
|
||||
- [ ] 点「重置」后回到:订单类型 NORMAL、出发日期今年至今、团期关键词为空
|
||||
|
||||
---
|
||||
|
||||
## 三、不影响范围
|
||||
|
||||
- 后端接口出入参、错误码、网关路由、权限码零变化;无 DDL、无 Flyway。
|
||||
- 小程序端零影响;其它调用 `groupBatchId` 的页面零影响。
|
||||
- 订单列表列渲染(含团单行「第N期 团期名」)保持原样。
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 需求出处:wx 2026-09-24 口述(圈出该筛选条件,要求去掉)
|
||||
- 关联工单:#8252(订单列表团期筛选改为按团期名/期次检索)、#8261(其实现 PR)、#7635(引入 `groupBatchId` 精确筛,本次撤掉其前端入口)
|
||||
- 后端:wx(本条目后端零改动)
|
||||
- 前端:mmg
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "车务派单看板的日期范围只按「当前年份」取数,跨年日期输入被静默忽略"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "b53414d4a6db2c17e39ae0ff450f148824642593"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-26"
|
||||
status_note: "浏览器实测(测试服 web.test.1814.love,2026-09-24 17:2x,车务管理员角色):派单看板 /fleet/board 的「日期范围」两个输入框显示为 MM-DD(默认 01-01 ~ 09-24),首次加载发出的请求是 /admin/fleet/board/orders?startDate=2026-01-01&endDate=2026-09-24(年份=当前年)。向输入框键入完整跨年日期(2027-01-01 / 2027-03-31)后:输入框文本确实变成新值,但再次点「搜索」**没有发出任何新请求**(board/orders 调用数保持 1),组件模型仍是旧区间。⇒ 2027 年出团的团期在本看板上无法通过日期筛选调出(同类数据在「团期配车」/fleet/group-dispatch 页可按 2027 日期筛出)。本条只提交互与可发现性:若产品定位就是「当年作业看板」,请在界面上标明或直接禁掉跨年输入;本月测试数据与明年真实排期多为 2027 年出团,看板够不着。 前端已修复并验证(2026-09-26):日期范围选择器 format 由 MM-dd 改 yyyy-MM-dd 带年份,跨年区间如实入参,用户已拍板支持跨年;默认区间仍当年 1.1 至今。"
|
||||
updated_at: "2026-09-24"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 车务派单看板 · 日期范围跨年不可达(输入被静默忽略)
|
||||
|
||||
> **端**: 管理后台 | **页面**: 车务管理 → 派单看板 `/fleet/board` | **Issue**: 无(交互/可发现性,需前端确认定位后处理)
|
||||
|
||||
---
|
||||
|
||||
## 一、现象(浏览器实测)
|
||||
|
||||
1. 打开派单看板(车务管理员角色),「日期范围」两个输入框显示 `01-01` 与 `09-24`(无年份);
|
||||
2. 首屏请求为 `/admin/fleet/board/orders?startDate=2026-01-01&endDate=2026-09-24&page=1&pageSize=20` —— **年份取当前年**;
|
||||
3. 在输入框键入完整日期 `2027-01-01` / `2027-03-31`:输入框文本**确实变成**新值,但再点「搜索」**没有发出任何新请求**(同一路径的调用数保持为 1),组件模型仍是旧区间;
|
||||
4. 结果:**2027 年出团的团期在派单看板上无法调出**。对照组:团期配车页 `/fleet/group-dispatch` 可以用 `departDateFrom/To=2027-*` 正常筛出 2027 年团期(实测列出 `Q202703102102983574083006466` 等)。
|
||||
|
||||
## 二、判断边界
|
||||
|
||||
- **不是后端缺陷**:接口按传入的 `startDate/endDate` 正常过滤,本条的输入框没把跨年区间传出去。
|
||||
- **可能是有意为之**(派单看板是「近期待派车」作业台,按当年取数是合理定位);因此本条目**不要求改成跨年可选**,只要求二者之一:
|
||||
1. 若定位就是当年 —— 在界面上标明(如占位文案写「本年度」),并对跨年输入给出反馈,**不要接受后又静默丢弃**;
|
||||
2. 若需要跨年 —— 让日期范围选择器带上年份并把区间如实传给接口。
|
||||
|
||||
## 三、影响范围
|
||||
|
||||
- **仅交互与可发现性**:`/fleet/board` 的日期范围控件与提示文案。
|
||||
- **零影响**:`/admin/fleet/board/orders`、`/board/summary` 的入参与过滤语义;团期配车、矩阵派单、车队对账等其它页面。
|
||||
@@ -0,0 +1,274 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8371"
|
||||
title: "取消派单:改期残留行单行取消,evidenceFileIds 改为按组累计"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "25d0469ccf12fc0fb2e359a8111e4acd133e6803"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-26"
|
||||
status_note: "PR #8380 已合并 dev-v3(commit c2bc5c3d4),测试服 hl-fleet-service 已部署该 commit(本单不改 order-v3),已在测试服完成 6 步全链路真实网关调用验证:残留行单行取消(不再命中 605027)、非残留组取消保持原逻辑、批量重提窗内行、需求确认成功。前端接入本次不需要自行判断「该行是否残留」——后端已在服务端自动判定并豁免日期门;唯一需要适配的是 CancelRespVO.evidenceFileIds 语义变化,见下文「六.6」。 前端 2026-09-26 已交付(hl-admin 25d0469c):isCanceledAssignmentResponse 认 canceled/exception 两态(通用入口取消在途残留行不再误报失败);残留清理入口注释与 cancelAssignment JSDoc 按本单订正(605710 透 message、evidenceFileIds 累计语义禁拼接——三调用点实证零消费响应值);sync-log 挂起项「DELETE 过去日期放行待确认」销项。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 取消派单:改期残留行单行取消,evidenceFileIds 改为按组累计
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service
|
||||
> **PR**: #8380
|
||||
> **Issue**: #8371
|
||||
> **日期**: 2026-09-26
|
||||
> **影响范围**: 管理后台车务派单「取消派车」弹窗与取消成功后的凭证展示
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
`DELETE /admin/fleet/assignments/{assignmentId}` 取消派车接口两处行为改动(路径、方法、入参字段不变):
|
||||
|
||||
1. **改期残留行单行取消**(`AssignmentController.java:464-481`、`AssignmentService.java:5308-5419`):订单改期后,落在「该行 `requirementId` 所指当前用车需求日期窗口」之外的旧逐日派车行(称「改期残留行」),调用本接口取消时现改为**只取消这一行**,不再连带同派车组内其他行。同时豁免 605027(已出发禁止整组取消)、605047(行程已结束只读)、605028(生效日不在派单服务日期范围内)三道日期门;`effectiveDate` 恒为今天,入参 `cutoffDate` 对残留行不生效。**非残留行(含全程行)行为不变,仍按派车组整组处理**。
|
||||
- 残留行判定依赖实时查询 order-v3 侧的用车需求日期窗(`orderQueryFacade.getFleetDetailContextStrict`),查询失败时**整笔取消 fail-closed,一行都不取消**,返回新错误码 605710「订单服务不可用:{0}」。
|
||||
2. **`CancelRespVO.evidenceFileIds` 语义变更**(`CancelRespVO.java:35`):改前是本次请求入参携带的凭证 ID 原样返回;改后是**该派车组自创建以来累计的全部取消凭证 ID**(追加式只增不丢,旧的在前新的在后),本次未传凭证时返回空数组,不代表没有历史凭证——历史凭证不会因为本次没传就消失。
|
||||
|
||||
另有一个非接口改动:夜间自动完结定时任务(`AssignmentCompleteJob` → `AssignmentService.listScheduleCompletableBefore`)现在会跳过判定为改期残留行的 `assigned` 状态派车,不再把它们自动置 `completed`;残留行会一直停在原状态,直到车务手工调用本接口取消。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 取消派单 | DELETE | `/admin/fleet/assignments/{assignmentId}` | 行为变更(出参字段语义变更,字段本身不变) | 改期残留行单行取消并豁免三道日期门;`evidenceFileIds` 改为按组累计 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 取消派单 `DELETE /admin/fleet/assignments/{assignmentId}`
|
||||
|
||||
**VO**: `CancelReqVO → CancelRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台车务派单管理页面,对某一行派车点击「取消」调用本接口,按条件释放该行占用的车辆/司机资源。目标行若被判定为「改期残留行」(服务日期落在当前用车需求日期窗之外),只取消这一行,不影响同派车组内窗口以内的其它日期行;否则仍按整组处理。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| assignmentId | Path | Long | 是 | 派单须存在,否则 605009 | 待取消的派单行 ID |
|
||||
| cancelReason | Body | String | 是 | `@NotBlank` | 取消原因 |
|
||||
| driverNotified | Body | Boolean | 是 | `@NotNull` | 是否已告知司机(人工存证,true/false 均可取消) |
|
||||
| notifyNote | Body | String | 否 | - | 告知方式/摘要备注 |
|
||||
| cutoffDate | Body | LocalDate | 否 | 服务端仅接受当天,空值按当天处理 | 取消生效日;**改期残留行本参数不生效,effectiveDate 恒为今天** |
|
||||
| evidenceFileIds | Body | Array\<Long\> | 否 | `@Size` 最多 20 个 | 本次新增的取消凭证文件 ID |
|
||||
| confirmWithoutEvidence | Body | Boolean | 否 | - | 无凭证时的二次确认标记 |
|
||||
| requestId | Body | String | 是 | `@NotBlank`,最长 64 | 幂等请求 ID |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.assignmentStatus | String | 取消后落库状态:来源行是 `unassigned` 落 `canceled`;来源行是 `assigned`/`holding`(在途)落 `exception`(与通用取消 #6152 同口径,非残留身份特有) |
|
||||
| data.assignmentGroupId | String(Long) | 派车组 ID;历史行无该字段时回退 assignmentId |
|
||||
| data.assignmentSlotId | String(Long) | 稳定车辆槽位 ID(历史字段) |
|
||||
| data.effectiveDate | LocalDate | 实际取消生效日;改期残留行恒为今天 |
|
||||
| data.evidenceFileIds | String(Long)[] | **本次改动**:该派车组自创建以来累计的全部取消凭证 ID(追加式只增不丢,旧在前新在后);本次未传凭证时为空数组,不代表没有历史凭证 |
|
||||
| data.confirmedWithoutEvidence | Boolean | 无凭证时是否已二次确认 |
|
||||
| data.sideEffects.vehicleStatusUpdated | String | 车辆状态更新结果(`idle`/`busy`/`maint`;不涉及车为 `null`) |
|
||||
| data.sideEffects.driverStatusUpdated | String | 司机状态更新结果(`busy`/`idle`;不涉及司机为 `null`) |
|
||||
| data.sideEffects.reconPrepRowsCreated | Integer | 对账预备单创建条数(M1 降级可能为 `null` 或 0) |
|
||||
| data.sideEffects.reconPrepMarkedCanceled | Integer | 对账预备单标记取消条数(M1 降级恒 0) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
> 测试服网关实测原文,目标行为改期残留行,服务日期已过去。
|
||||
|
||||
```json
|
||||
{
|
||||
"cancelReason": "8371-verify-residue-delete",
|
||||
"driverNotified": true,
|
||||
"notifyNote": "8371-verify-residue-delete",
|
||||
"confirmWithoutEvidence": true,
|
||||
"requestId": "8371-verify-delete-1"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
> 测试服网关实测原文。取消前该行处于 `assigned`,故落库为 `exception` 而非 `canceled`;`reconPrepRowsCreated` 实测返回 `null`,前端勿据它做判断。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"assignmentStatus": "exception",
|
||||
"assignmentGroupId": "362105579390504961",
|
||||
"assignmentSlotId": "362105579390504961",
|
||||
"effectiveDate": "2026-09-26",
|
||||
"evidenceFileIds": [],
|
||||
"confirmedWithoutEvidence": true,
|
||||
"sideEffects": {
|
||||
"vehicleStatusUpdated": "busy",
|
||||
"driverStatusUpdated": "busy",
|
||||
"reconPrepRowsCreated": null,
|
||||
"reconPrepMarkedCanceled": 0
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本端点是单条写操作,无空数据态。目标行判定改期残留行失败(依赖的 order-v3 查询不可达)时,不返回部分结果、不猜测是否残留,一行都不取消,直接走错误响应(见下方 605710)。
|
||||
|
||||
```json
|
||||
{ "code": 605710, "message": "订单服务不可用:取改期残留判定所需的用车需求日期窗失败", "success": false }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 605009, "message": "派单不存在", "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 605026, "message": "未上传取消凭证,请二次确认后再取消", "success": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 605027, "message": "行程已出发,不能整组取消,请使用修改派单", "success": false }
|
||||
```
|
||||
|
||||
**其它错误码**:400(`cancelReason`/`driverNotified`/`requestId` 请求校验)/ 605020「当前派单状态不允许此操作」/ 605028「生效日期不在派单服务日期范围内」/ 605047「行程已结束,派车信息只读,不能修改或改派」(`AssignmentService.java:5410` `assertTripMutable`)。**605027/605047/605028 三码仅在目标行判定为非改期残留行时才可能命中**;判定为改期残留行时豁免这三码,改走单行取消分支。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **改期残留行判定**(`AssignmentDailyPlanFacts.isRescheduleResidue`,经 `AssignmentService.resolveCancelResidueWindow`/`isCancelResidueRow` 调用):仅逐日行(非全程行)参与判定;判定要在加锁前实时查询 order-v3 侧当前用车需求的日期窗,查询失败直接抛 605710,不对残留身份做任何假设,不取消任何行。
|
||||
- **单行取消**:残留行确认后,只取消这一行;同派车组内落在窗口以内的其它日期行不受影响、不校验、不触发状态机。
|
||||
- **整组取消**:非残留行维持原逻辑——取消会按派车组整体处理(`resolveEffectiveGroupRows` 圈定的全部可取消行),605027/605047/605028 三道日期门原样校验,`effectiveDate` 仍取 `cutoffDate`(默认今天,只接受今天)。
|
||||
- **落库状态并非恒为 `canceled`**:来源行状态是 `unassigned` 时落 `canceled`;是 `assigned`/`holding`(在途)时落 `exception`(#6152 既有分流规则,不因残留身份而不同)。
|
||||
- **凭证累积**:命中残留分支时同组会分多次调用逐行取消,若整组替换式返回本次入参会丢掉前几次的凭证;因此 `evidenceFileIds` 统一改为按组读取累计清单(`assignmentEvidenceService.listCancellationFileIds(groupId)`),前端应直接以响应值为准,不要自行拼接历史值。
|
||||
- **操作日志**:仅命中残留分支时,`assignmentOperationLog` 的 `CANCEL_REQUESTED` 记录 `detailJson` 会带 `"scope":"RESIDUE_ROW"`;非残留(整组)分支不写这个键(不是写 `"GROUP"`,而是完全没有该字段)。可通过 `GET /admin/fleet/board/orders/{orderId}` 的 `operationLog[].detailJson` 读取核对。
|
||||
- **司机通知**:`driverNotified=false` 仍允许取消,仅作人工存证,后端不代为通知。
|
||||
- **幂等性**:`requestId` 走 `@Idempotent`,10 秒窗口内重复提交被拦截(提示「派单取消处理中,请勿重复提交」)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 残留行判定是服务端自动完成的,前端不需要预判
|
||||
|
||||
前端调用取消接口时无需先判断目标行是否为「改期残留行」——服务端在处理请求时会自动查询、自动分流。前端只需按现有交互正常收集 `cancelReason`/`driverNotified`/`requestId` 等必填参数发起请求:
|
||||
- 命中残留行分支时,响应正常返回 200,`effectiveDate` 恒为今天,同派车组内其它行不受影响;前端无需额外处理。
|
||||
- 判定依赖的 order-v3 查询失败时,返回 605710,此时**一整行都没有取消**,前端应按普通失败提示处理并允许重试,不需要区分"是不是残留行"。
|
||||
|
||||
### evidenceFileIds 的正确使用方式
|
||||
|
||||
本次请求**携带凭证**时,`CancelRespVO.evidenceFileIds` 返回该派车组截至本次累计的全部取消凭证 ID,旧在前新在后(而不是本次请求入参那几个;`AssignmentService.java:5526-5527`)。前端展示「该派车组已上传哪些取消凭证」时,应直接使用响应值整体覆盖式渲染,不要再把它与本地缓存的历史值做拼接,否则会出现重复 ID。
|
||||
|
||||
若本次请求未携带任何凭证(`evidenceFileIds` 为空、以 `confirmWithoutEvidence=true` 放行 605026),响应的 `evidenceFileIds` 为空数组——这不代表该派车组没有历史凭证,只是本次响应未重新拉取累计列表(`hasEvidence` 为 false 分支不会调用 `listCancellationFileIds`)。因此前端不要用空数组覆盖已展示的历史凭证;需要展示累计凭证时,以最近一次**携带凭证**的取消响应为准。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- **`fleet_assignment`**:目标行 `assignment_status` 更新——来源行是 `unassigned` 写 `canceled`,是 `assigned`/`holding`(在途)写 `exception`(#6152 既有分流规则,不因是否残留行而不同);`updated_by` 显式落取消操作人。无表结构变更、无新增列。
|
||||
- **`fleet_assignment_evidence`**:`hasEvidence=true` 时追加新行(`assignmentEvidenceService.appendCancellationEvidenceValidated`),不删除、不覆盖已有记录。
|
||||
- **`fleet_assignment_operation_log`**:追加一条 `CANCEL_REQUESTED` 记录;`detail_json` 命中残留行分支时带 `"scope":"RESIDUE_ROW"`,非残留(整组)分支**不写 `scope` 键**(不是写 `"GROUP"`)。`hasEvidence=true` 时另追加一条 `CANCEL_EVIDENCE_RECORDED` 记录。
|
||||
- **无新增表、无新增列**:改期残留行的判定结果本身不落库,每次取消请求都会重新实时查询 order-v3。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **未登录** → 401(网关拦截)。
|
||||
- **派单不存在** → 605009。
|
||||
- **状态不允许**(如已取消、已完结) → 605020。
|
||||
- **order-v3 不可达** → 605710 fail-closed,一行都不取消,不对残留身份做任何猜测。
|
||||
- **无凭证未二次确认** → 605026。
|
||||
- **非残留行已出发** → 605027;**非残留行程已结束** → 605047;**非残留行生效日不在服务日期范围内** → 605028——这三码只在非残留分支可能命中,残留行分支豁免。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 行为对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 取消改期残留行 | 按整组处理,605027/605047/605028 三道日期门正常校验,同组内已出发的其它行会导致整组取消被拒 | **新增单行分支**:只取消该行本身,豁免三道日期门,`effectiveDate` 固定今天;同组其它行不受影响 |
|
||||
| order-v3 查询失败时的处理 | 不涉及(改前无此依赖) | 新增 fail-closed:605710,一行都不取消 |
|
||||
| 夜间自动完结 | 残留行与其它行一样,到期后被 `AssignmentCompleteJob` 自动置 `completed` | 残留行被跳过,停留在原状态直到车务手工调用本接口取消 |
|
||||
|
||||
### 出参字段对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `evidenceFileIds` | 本次请求入参携带的凭证 ID 原样返回 | **语义变更**:该派车组自创建以来累计的全部取消凭证 ID(`hasEvidence=true` 时返回累计列表,为 false 时返回空数组) |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否。改期残留行的单行取消是新增的判定分支,非残留行(含全程行)的整组取消逻辑、三道日期门校验逐字不变。`evidenceFileIds` 字段名、类型(`Array<Long>`/序列化为字符串数组)均未变,只是同一个字段返回的内容集合变了。
|
||||
- **前端是否必须同步上线**:否。前端不做任何改动,取消接口对非残留行的调用行为与改前完全一致;对残留行的调用会自动享受到"不再报 605027/605047/605028"的效果,不需要前端识别或适配。**唯一需要前端注意的**:如果前端此前把 `evidenceFileIds` 当作"本次上传的文件"做本地拼接展示,接入后应改为直接使用响应值整体替换,否则会展示出重复的凭证 ID。
|
||||
- **覆盖边界**:本次改动不影响 `POST /admin/fleet/assignments/batch`、`POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 等相邻写口自身的契约,只是这些写口在残留行被移出组后不再对该组报改前的连带错误(100001/605055),详见「八、测试环境已验证」步骤 4/6 的实测记录。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:`DELETE /admin/fleet/assignments/{assignmentId}` 对改期残留行的判定分支,以及 `evidenceFileIds` 的取值口径。
|
||||
- **零影响**:
|
||||
- 非残留行(含全程行)的整组取消规则,三道日期门校验逐字不变。
|
||||
- 派车创建、批量重提、需求确认等其它写口自身的入参/出参契约。
|
||||
- 车辆/司机占用释放的条件判断逻辑(`sideEffects` 字段结构不变)。
|
||||
- #6152 既定的取消状态分流规则(`unassigned`→`canceled`,`assigned`/`holding`→`exception`)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:PR #8380 已合并 `dev-v3`(`c2bc5c3d4`),测试服 hl-fleet-service 已部署该 commit;本单不改 order-v3。
|
||||
|
||||
以下 6 步为同一夹具的全链路真实网关调用(夹具:`orderId=2103714435619303425`,改期后需求窗 2026-10-02~10-04,派车组 3 行,其中 2026-09-20 一行落在窗外):
|
||||
|
||||
1. **改前状态**:`GET /admin/fleet/board/orders/{orderId}` 确认三行初始状态均为 `assigned`。
|
||||
2. **DELETE 残留行**:对 2026-09-20 这一行发起取消,改前同一请求体命中 605027(拒绝整组取消);改后单行取消直接成功,返回 `assignmentStatus="exception"`(该行取消前是 `assigned`,按 #6152 分流落 `exception`)、`evidenceFileIds=[]`。
|
||||
3. **复读验证**:窗内两行逐字段(vehicleId/driverId/assignmentPrice/priceSource/assignmentStatus 等)与改前基线完全一致,未受影响;操作日志新增一条 `detailJson` 含 `"scope":"RESIDUE_ROW"` 的记录。
|
||||
4. **批量重提窗内行**:`POST /admin/fleet/assignments/batch` 提交窗内两行,改前同一请求体命中 100001("已过去或已完结,不能修改逐日派车配置");改后不再报错(残留行已单独取消,不再纳入本次提交的校验范围)。
|
||||
5. **取新 generation**:复读 `dispatchPlanGeneration` 供确认步骤使用。
|
||||
6. **需求确认**:`POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 成功,改前基线在此步命中 605055("派车方案已变化,请刷新后重试"),改后成功确认。
|
||||
|
||||
全链路(DELETE 残留行 → 批量重提窗内行 → 需求确认)打通,改前基线记录的三处拒绝(605027/100001/605055)在改后代码下不再复现。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8371](https://git.1814.love/wx/HL/issues/8371)
|
||||
- 关联 PR: [wx/HL#8380](https://git.1814.love/wx/HL/pulls/8380)
|
||||
- 上游工单: #5935(改期残留清理,见 `20_5935_改期残留清理用现有读写口即可-前端缺陷-管理后台.md` 内 9 处订正指针)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8371](https://git.1814.love/wx/HL/issues/8371)
|
||||
- **PR**: [#8380](https://git.1814.love/wx/HL/pulls/8380)
|
||||
- **Merge commit**: [c2bc5c3d4](https://git.1814.love/wx/HL/commit/c2bc5c3d4)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,347 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8372"
|
||||
title: "团期派车总览 days[].vehicles[] 新增 groupCode 字段回显乘车分组编码"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ddd49b07d332b753a4513f01f63ed2fc9495f888"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-26"
|
||||
status_note: "PR #8378 已合并 dev-v3(f8b546251),测试服 hl-fleet-service 已部署 c2bc5c3d4(含 f8b546251),本单不改 order-v3。测试环境完成两批实测:既存真实批次(groupBatchId=2101880394750328833)确认 9 条 vehicles 均正确回显 groupCode;自建全新批次(groupBatchId=2103733823122690049)完整跑通零变更重提(addedCount/removedCount/updatedCount 均为 0,idempotentShortCircuit=true)与删行/加行往返。 前端 2026-09-26 已交付(hl-admin ddd49b07,用户拍板回显闭环本期做):PlanEditor 新增 overview prop 按 groupCode+tripDate 回显现行计划(车辆/司机/备注,标签取车牌司机名);groupCode=null 历史行点名列出+提交前二次确认(全量替换语义);OverviewDrawer 传入 overview;api JSDoc 补 groupCode/窗外行口径。PlanEditor spec 11 例全绿(新增回显 3 例)。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期派车:总览新增乘车分组编码回显
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-fleet-service
|
||||
> **PR**: #8378
|
||||
> **Issue**: #8372
|
||||
> **日期**: 2026-09-26
|
||||
> **影响范围**: 管理后台「团期配车 → 待配车团期 → 总览」页面逐车行显示
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
`GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview` 响应的 `days[].vehicles[]` 每行新增字段 `groupCode`(String,乘车分组键,如 `"A"`/`"BUS"`,历史未分组行为 `null`)。该字段原样取自 `fleet_group_dispatch.group_id`(`GroupDispatchOverviewVehicleVO.java:59-70`),供前端在「重新配置」表单里把总览原样重组成 reconfigure 请求:`groupCode` 对应 reconfigure 入参 `demands[].assignments[].groupId`,`vehicleId`/`driverId`/`remark` 同样原样回填。
|
||||
|
||||
reconfigure 是全量替换语义,请求里缺席的 (日期, 车) 会被软删;历史未分组行 `groupCode` 为 `null` 时,回提前必须先引导车务补选分组,否则 `groupId` 的 `@NotBlank` 校验会拒绝(400「乘车分组不能为空」)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期派车总览 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | 出参新增字段 | `days[].vehicles[].groupCode` 回显乘车分组键 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期派车总览 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview`
|
||||
|
||||
**VO**: `(无请求体,路径参数 groupBatchId) → GroupDispatchOverviewRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台「团期配车」页面,进入某团点击「总览」查看该团逐日已排车、空洞日与逐户接送机缺口。前端把这份总览原样重组即可拼出 reconfigure 的「重新配置」请求,实现「查看 → 调整 → 重新提交」的闭环;本次新增的 `groupCode` 就是这条回提链路里此前缺失的一环。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | 团期须存在 | 团期主订单 ID(雪花) |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String(Long) | 团期主订单 ID |
|
||||
| batchNo | String | 团号 |
|
||||
| departDate | String(LocalDate) | 出团日 |
|
||||
| endDate | String(LocalDate) | 返团日 |
|
||||
| serviceDates | Array\<String\> | 权威服务日集合(order-v3 下发,fleet 不自算) |
|
||||
| requirementConfirmed | Boolean | 整团需求是否已确认 |
|
||||
| vehicleReady | Boolean | 配车是否已就绪 |
|
||||
| days | Array\<Object\> | 逐日行(按 serviceDates 顺序铺满,空洞日也占一行) |
|
||||
| days[].tripDate | String(LocalDate) | 行程日 |
|
||||
| days[].vehicles | Array\<Object\> | 当日已排车项(无则空数组) |
|
||||
| days[].vehicles[].dispatchId | String(Long) | 团期配车行 ID |
|
||||
| days[].vehicles[].vehicleId | String(Long) | 车辆 ID |
|
||||
| days[].vehicles[].vehiclePlate | String | 车牌(车已删则为 null) |
|
||||
| days[].vehicles[].vehicleModel | String | 车型名(车已删则为 null) |
|
||||
| days[].vehicles[].driverId | String(Long) | 司机 ID(仅排车未排司机时为 null) |
|
||||
| days[].vehicles[].driverName | String | 司机姓名(未排司机或司机已删则为 null) |
|
||||
| days[].vehicles[].driverPhone | String | 司机手机(脱敏) |
|
||||
| days[].vehicles[].status | String | 派车状态:`ASSIGNED`/`CONFIRMED` |
|
||||
| days[].vehicles[].remark | String | 备注 |
|
||||
| days[].vehicles[].groupCode | String | **新增**:乘车分组键,原样取自 `fleet_group_dispatch.group_id`,存量未分组行为 `null` |
|
||||
| days[].vehicleCount | Integer | 当日已排车辆数 |
|
||||
| days[].dispatched | Boolean | 当日是否已排车(vehicleCount > 0) |
|
||||
| missingDates | Array\<String\> | 空洞日(serviceDates 中没有任何存活派车行的日期) |
|
||||
| orders | Array\<Object\> | 逐户行 |
|
||||
| orders[].orderId | String(Long) | 子订单 ID |
|
||||
| orders[].orderNo | String | 订单号 |
|
||||
| orders[].customerName | String | 客户姓名 |
|
||||
| orders[].headcount | Integer | 出行人数 |
|
||||
| orders[].vehicleControlStatus | String | 用车管控状态(订单侧口径) |
|
||||
| orders[].travelRequirementId | String(Long) | 该户当前有效用车需求 ID(无则为 null) |
|
||||
| orders[].travelRequirementStatus | String | 该户当前有效用车需求状态(无则为 null) |
|
||||
| orders[].transferDeclared | Boolean | 是否声明接送机 |
|
||||
| orders[].transferArrivalDates | Array\<String\> | 接机声明日期(原始值,可在行程日窗外) |
|
||||
| orders[].transferDepartureDates | Array\<String\> | 送机声明日期(原始值,可在行程日窗外) |
|
||||
| orders[].transferPickupCoveredDates | Array\<String\> | 已派接机车的服务日 |
|
||||
| orders[].transferDropoffCoveredDates | Array\<String\> | 已派送机车的服务日 |
|
||||
| orders[].transferPendingCount | Integer | 该户接送机未配计数 |
|
||||
| transferPendingTotal | Integer | 全团接送机未配计数(= orders[].transferPendingCount 之和) |
|
||||
| conversationKey | String | 团期车务会话键,形如 `GROUP_FLEET:{groupBatchId}` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/group-dispatch/batches/2103733823122690049/overview HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
> 测试服网关实测原文(自建夹具,3 天 × 2 组共 6 条 vehicles,逐条均带 groupCode)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2103733823122690049",
|
||||
"batchNo": "Q202703152103733770773524481",
|
||||
"departDate": "2027-03-15",
|
||||
"endDate": "2027-03-17",
|
||||
"serviceDates": ["2027-03-15", "2027-03-16", "2027-03-17"],
|
||||
"requirementConfirmed": true,
|
||||
"vehicleReady": false,
|
||||
"days": [
|
||||
{
|
||||
"tripDate": "2027-03-15",
|
||||
"vehicles": [
|
||||
{
|
||||
"dispatchId": "2103736241809952770",
|
||||
"vehicleId": "2065329514971308033",
|
||||
"vehiclePlate": "蒙C02E02",
|
||||
"vehicleModel": "丰田考斯特",
|
||||
"driverId": "2089691297869651969",
|
||||
"driverName": "P3测试司机18",
|
||||
"driverPhone": "139****0018",
|
||||
"status": "ASSIGNED",
|
||||
"remark": "8372-verify",
|
||||
"groupCode": "A"
|
||||
},
|
||||
{
|
||||
"dispatchId": "2103736241814147074",
|
||||
"vehicleId": "2065329516997156865",
|
||||
"vehiclePlate": "蒙C08E08",
|
||||
"vehicleModel": "丰田考斯特",
|
||||
"driverId": "2089691294107361282",
|
||||
"driverName": "P3测试司机17",
|
||||
"driverPhone": "139****0017",
|
||||
"status": "ASSIGNED",
|
||||
"remark": "8372-verify",
|
||||
"groupCode": "B"
|
||||
}
|
||||
],
|
||||
"vehicleCount": 2,
|
||||
"dispatched": true
|
||||
},
|
||||
{
|
||||
"tripDate": "2027-03-16",
|
||||
"vehicles": [
|
||||
{"dispatchId": "2103736241818341377", "vehicleId": "2065329514971308033", "vehiclePlate": "蒙C02E02", "vehicleModel": "丰田考斯特", "driverId": "2089691297869651969", "driverName": "P3测试司机18", "driverPhone": "139****0018", "status": "ASSIGNED", "remark": "8372-verify", "groupCode": "A"},
|
||||
{"dispatchId": "2103736241822535682", "vehicleId": "2065329516997156865", "vehiclePlate": "蒙C08E08", "vehicleModel": "丰田考斯特", "driverId": "2089691294107361282", "driverName": "P3测试司机17", "driverPhone": "139****0017", "status": "ASSIGNED", "remark": "8372-verify", "groupCode": "B"}
|
||||
],
|
||||
"vehicleCount": 2,
|
||||
"dispatched": true
|
||||
},
|
||||
{
|
||||
"tripDate": "2027-03-17",
|
||||
"vehicles": [
|
||||
{"dispatchId": "2103736241826729986", "vehicleId": "2065329514971308033", "vehiclePlate": "蒙C02E02", "vehicleModel": "丰田考斯特", "driverId": "2089691297869651969", "driverName": "P3测试司机18", "driverPhone": "139****0018", "status": "ASSIGNED", "remark": "8372-verify", "groupCode": "A"},
|
||||
{"dispatchId": "2103736241830924290", "vehicleId": "2065329516997156865", "vehiclePlate": "蒙C08E08", "vehicleModel": "丰田考斯特", "driverId": "2089691294107361282", "driverName": "P3测试司机17", "driverPhone": "139****0017", "status": "ASSIGNED", "remark": "8372-verify", "groupCode": "B"}
|
||||
],
|
||||
"vehicleCount": 2,
|
||||
"dispatched": true
|
||||
}
|
||||
],
|
||||
"missingDates": [],
|
||||
"orders": [
|
||||
{"orderId": "2103733822850060289", "orderNo": "HL20260926143003310", "customerName": "8372verify客户A", "headcount": 2, "vehicleControlStatus": "PENDING", "travelRequirementId": "2103735427204894722", "travelRequirementStatus": "PENDING", "transferDeclared": false, "transferArrivalDates": [], "transferDepartureDates": [], "transferPickupCoveredDates": [], "transferDropoffCoveredDates": [], "transferPendingCount": 0},
|
||||
{"orderId": "2103733866504335361", "orderNo": "HL20260926143013865", "customerName": "8372verify客户B", "headcount": 2, "vehicleControlStatus": "PENDING", "travelRequirementId": "2103735431386574850", "travelRequirementStatus": "PENDING", "transferDeclared": false, "transferArrivalDates": [], "transferDepartureDates": [], "transferPickupCoveredDates": [], "transferDropoffCoveredDates": [], "transferPendingCount": 0}
|
||||
],
|
||||
"transferPendingTotal": 0,
|
||||
"conversationKey": "GROUP_FLEET:2103733823122690049"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
某行程日没有任何存活派车行时,该日在 `days[]` 里仍占一行,`vehicles` 为空数组、`dispatched=false`,并计入 `missingDates`;不会因此报错,也不影响其它日期的正常返回。团期整体不可达(团期不存在、order-v3 侧基线查询失败或降级)时整口失败关闭,返回 600012「团期配车基线不可达,请稍后重试」(`GroupDispatchErrorCode.java:95`),不会返回空 `days[]`;前端须按失败提示处理,**不要渲染成「该团没有配车需求」**(`GroupDispatchQueryController.java:90`)。此行为本次未改动。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
> 团期配车基线不可达(`GroupDispatchErrorCode.java:95`,失败关闭,HTTP 状态 200)。
|
||||
|
||||
```json
|
||||
{"code": 600012, "message": "团期配车基线不可达,请稍后重试", "data": null, "success": false}
|
||||
```
|
||||
|
||||
> 网关鉴权失败的通用响应(`JwtAuthFilter.java:394`,HTTP 状态按项目铁律固定 200,业务码 401)。
|
||||
|
||||
```json
|
||||
{"code": 401, "message": "缺少有效的 Authorization 头", "data": null, "success": false}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `groupCode` 原样取自 `fleet_group_dispatch.group_id`,不做转换或聚合;写口入参字段名是 `groupId`,读口字段名是 `groupCode`,同值同源(`GroupDispatchOverviewVehicleVO.java:59-70`)。
|
||||
- 存量未分组行 `groupCode` 为 `null`,不回填默认组;这类行提交 reconfigure 时若未先补选分组,会在参数校验层被 `@NotBlank` 拦截(400「乘车分组不能为空」),不会进入服务层业务码判断。
|
||||
- 回提映射经测试服实测:按 overview 原样重组回提,差量为零(`addedCount`/`removedCount`/`updatedCount` 均为 0,`idempotentShortCircuit=true`)。
|
||||
- 若回提时删除某乘车分组在某日仅有的一辆车、导致该组该日出现覆盖缺口,会触发 reconfigure 既有的覆盖校验,返回 602003「乘车分组 X 的服务日未排满, 缺失: YYYY-MM-DD」(测试服实测)——这是 reconfigure 早已存在的门禁,不是本次改动新增的行为。
|
||||
- `serviceDates` 权威口径来自 order-v3 单团覆盖口,fleet 不按 `departDate..endDate` 自行铺日期,避免总览与 reconfigure 的覆盖校验形成两套口径(`GroupDispatchOverviewRespVO.java:14-19`)。
|
||||
- **总览不含服务日窗外的配车行**:`days[]` 只按 `serviceDates` 逐日铺开(`GroupDispatchQueryService.java:558`),`tripDate` 不在 `serviceDates` 内的存活配车行(例如需求改期后遗留在旧日期上的行)不会出现在总览里,因此按总览重组的 reconfigure 请求也不含这些行。reconfigure 是全量替换语义,回提后这些行会被软删;若本次提交处于重开窗口授权下,则会被 602013「本次配车改动越出重开窗口授权范围: {0}」拒绝(`GroupDispatchService.java:432`)。这是既有的全量替换语义与总览铺日口径叠加的结果,本次未改动。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 与 reconfigure 的正确回提映射
|
||||
|
||||
overview 每行 `days[].vehicles[].groupCode` 对应 reconfigure 入参 `demands[].assignments[].groupId`(`GroupDispatchAssignmentReqVO.java:20-24`,`@NotBlank`);`vehicleId`/`driverId`/`remark` 同样原样回填。reconfigure 是全量替换语义,请求里缺席的 (日期, 车) 会被判定为软删——前端必须把 overview 读到的每一行都带回,漏一行就是删一行。
|
||||
|
||||
实测原样回提请求(对应上文「响应示例」同一批夹具,`requirementId`/`requirementVersion` 取自 `GET .../readiness`):
|
||||
|
||||
```json
|
||||
{
|
||||
"requirementId": 2103735470083264513,
|
||||
"requirementVersion": 2,
|
||||
"clearAll": false,
|
||||
"demands": [
|
||||
{
|
||||
"tripDate": "2027-03-15",
|
||||
"assignments": [
|
||||
{"groupId": "A", "vehicleId": 2065329514971308033, "driverId": 2089691297869651969, "remark": "8372-verify"},
|
||||
{"groupId": "B", "vehicleId": 2065329516997156865, "driverId": 2089691294107361282, "remark": "8372-verify"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2027-03-16",
|
||||
"assignments": [
|
||||
{"groupId": "A", "vehicleId": 2065329514971308033, "driverId": 2089691297869651969, "remark": "8372-verify"},
|
||||
{"groupId": "B", "vehicleId": 2065329516997156865, "driverId": 2089691294107361282, "remark": "8372-verify"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2027-03-17",
|
||||
"assignments": [
|
||||
{"groupId": "A", "vehicleId": 2065329514971308033, "driverId": 2089691297869651969, "remark": "8372-verify"},
|
||||
{"groupId": "B", "vehicleId": 2065329516997156865, "driverId": 2089691294107361282, "remark": "8372-verify"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
对应响应确认零差量:`addedCount=0`/`removedCount=0`/`updatedCount=0`/`keptCount=6`/`aliveCount=6`/`idempotentShortCircuit=true`。
|
||||
|
||||
若 overview 中某行 `groupCode` 为 `null`,需先引导车务选择分组再回填 `groupId`,否则 reconfigure 返回 400「乘车分组不能为空」(`@NotBlank` 校验先于服务层业务码触发)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无本次 DDL 变更。`groupCode` 直接读取既有列 `fleet_group_dispatch.group_id`(该列由更早的迁移引入,历史行未回填、值为 `NULL`),本次改动只是让这个已存在的列首次通过 overview 出参外露,不涉及表结构变更或数据回填。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 网关鉴权失败(缺少/失效 Authorization)→ 业务码 401,HTTP 状态仍为 200。
|
||||
- 团期不存在或 order-v3 基线不可达 → 600012「团期配车基线不可达,请稍后重试」,不返回空数据。
|
||||
- `groupCode` 为 `null` 的行按 `null` 原样返回,不做默认值兜底(`GroupDispatchOverviewVehicleVO.java:64`:「存量行此列为 NULL 且不回填,这里原样透出 null,不补空串或默认组」)。
|
||||
- 同一辆车在不同日可属不同分组,按行独立取值,overview 不做跨日聚合或去重。
|
||||
- 空洞日(无存活派车行的服务日)在 `days[]` 里仍占一行,`vehicles=[]`、`dispatched=false`,同时计入 `missingDates`。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 出参字段对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `days[].vehicles[].groupCode` | 无此字段 | **新增**:String,乘车分组键,原样取自 `fleet_group_dispatch.group_id`,存量未分组行为 `null` |
|
||||
|
||||
### 行为对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| overview → reconfigure 回提 | 前端拿不到分组键,需要自行维护「车辆→分组」映射表才能拼出 `assignments[].groupId` | 直接用 `groupCode` 原样回填,无需自建映射表 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**:否。仅新增一个出参字段,既有字段语义、类型均未变,历史客户端可直接忽略新字段。
|
||||
- **前端是否必须同步上线**:否。`groupCode` 是可选回显字段,前端不接入仍可正常工作;若前端此前已自行维护一份「车辆→分组」映射表来拼 reconfigure 请求,接入后可直接用响应字段替代那份映射表,避免两处维护不同步的风险。
|
||||
- **覆盖范围**:本字段只出现在总览读口的出参里,reconfigure 写口的入参/出参契约(含 `groupId` 字段本身的必填与长度校验)未发生变化。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:团期派车总览 `GET .../overview` 的出参新增一个字段。
|
||||
- **零影响**:
|
||||
- reconfigure 写口契约(入参/出参结构、错误码均未变)。
|
||||
- 待配车团期清单 `GET .../pending-batches`、资源排班等其他读口。
|
||||
- 车务其它模块(含 #8371 涉及的单车取消逻辑)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:PR #8378 已合并 `dev-v3`(`f8b546251`),测试服 hl-fleet-service 已部署 `c2bc5c3d4`(含 `f8b546251`);本单不改 order-v3。
|
||||
|
||||
**批次一(既存真实数据)**:`groupBatchId=2101880394750328833`,GET overview 9 条 vehicles(3 天 × 3 组)逐条核对均带 `groupCode`(GA/GB/GC),与改前基线(9 条均无该字段)对照修复生效;该批次底层需求已到 `DONE` 终态,零变更重提子项在此批次上不可达(非缺陷,既存数据状态限制)。附加 null 探测:`groupId=null` 返回 400「乘车分组不能为空」,先于 602006 业务码触发。
|
||||
|
||||
**批次二(自建全新夹具)**:`groupBatchId=2103733823122690049`,全程停留在 `CONFIRMED`(未推进到 `DONE`),补齐批次一未能验证的子项:
|
||||
- 零变更重提:`addedCount`/`removedCount`/`updatedCount` 均为 0,`idempotentShortCircuit=true`。
|
||||
- 删行/加行往返:先给组 A 追加 1 辆备用车(`addedCount=1`),再删除该行(`removedCount=1`,2027-03-15 的 `vehicleCount` 从 3 降为 2,其余两日不受影响),最后加回(`addedCount=1`,`aliveCount` 恢复到 7)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8372](https://git.1814.love/wx/HL/issues/8372)
|
||||
- 关联 PR: [wx/HL#8378](https://git.1814.love/wx/HL/pulls/8378)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8372](https://git.1814.love/wx/HL/issues/8372)
|
||||
- **PR**: [#8378](https://git.1814.love/wx/HL/pulls/8378)
|
||||
- **Merge commit**: [f8b546251](https://git.1814.love/wx/HL/commit/f8b546251)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8373"
|
||||
title: "下线车控「我的接单」接口 GET /v3/admin/order/grab-pool/my-claims/vehicle(恒返回空页)"
|
||||
consumer: "admin"
|
||||
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: "车务没有抢单业务,这个接口对所有账号都返回空页(用车需求从不写入抢单人),因此下线。hl-ui origin/v2.1 136973bf 无调用方(同命令能搜到 my-claims/hotel,作阳性对照);测试服网关日志 09-18~09-26 第三方调用 0 次。13_7439 §5 承诺的 kind 入参与 records[].kind 从未实现,随本件作废。 前端 2026-09-27 复核(mmg):grep 实证 my-claims/vehicle 在 hl-admin origin/v2.1 全仓零命中,零代码改动;19_7443 遗留「我的接单 vehicle 分栏」同步关闭,车务归属按 #8342 操作车务口径。维持 not_required。
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:下线车控「我的接单」接口 `GET /v3/admin/order/grab-pool/my-claims/vehicle`
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8377(dev-v3 `1ac676a85`)
|
||||
**Issue**: #8373
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **这个接口从来没有返回过数据。** 它按「抢单人 = 当前登录人」过滤用车需求,但用车需求从不写入抢单人:唯一写这一列的 Mapper 方法把它置为 null,新建用车需求的路径也不写它。所以对任何账号,它都返回 200 空页。
|
||||
|
||||
🔴 **车务没有抢单业务**(wx 2026-09-26 定)。车务的归属看订单详情里的「操作车务」(#8342),不看抢单人。
|
||||
|
||||
🔴 **13_7439 §5 作废。** 那一节写的 `kind` 入参和 `records[].kind` 出参从未实现:入参 VO 只有 `status` 一个字段,kind 过滤在 Service 里从未赋值。该节标题下已加订正指针,指向本件。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
这个接口是房务审计 D1 时从 Mock 改成真实查询的,照搬了房务侧「我的接单」的做法。但房务有「抢单 → 写入抢单人」的写入链路,车务没有,所以车务这一侧的读链路一开始就查不到任何行。
|
||||
|
||||
2026-09-26 前端按 13_7439 §5 提出「车务我的抢单缺 kind」。wx 定车务没有抢单,本单据此下线这个接口,而不是补 kind。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 我的接单(车控视角) | GET | `/v3/admin/order/grab-pool/my-claims/vehicle` | 删除 | 接口整体下线;下线前对所有账号恒返回空页 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 我的接单(车控视角) `GET /v3/admin/order/grab-pool/my-claims/vehicle`
|
||||
|
||||
**VO**: `MyClaimsQueryReqVO → Result<PageResult<VehicleClaimItemVO>>`(两个 VO 都已删除)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
**已下线,不再有使用场景。** 车务没有抢单业务。要看某个订单归哪位车务,用订单详情里的「操作车务」(#8342)。
|
||||
|
||||
#### 入参
|
||||
|
||||
**已下线,不再接受任何入参。** 以下是下线前的实际形态,仅供核对调用代码:
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| status | Query | String | 否 | PROCESSING / DONE / ALL,默认 ALL | **已下线** |
|
||||
| page | Query | Integer | 否 | ≥1,默认 1(也接受 `pageNo`) | **已下线** |
|
||||
| pageSize | Query | Integer | 否 | 1~100,默认 20 | **已下线** |
|
||||
| kind | Query | String | 否 | — | **从未实现**。13_7439 §5 写了它,但代码里入参 VO 没有这个字段,传了也不生效 |
|
||||
|
||||
#### 出参
|
||||
|
||||
**已下线,不再有响应体。** 下线前的形态:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records | Array | **已下线**。下线前对所有账号恒为空数组 |
|
||||
| records[].kind | String | **从未实现**。13_7439 §5 写了它,`VehicleClaimItemVO` 里从来没有这个字段 |
|
||||
| total | Integer | **已下线**。下线前恒为 0 |
|
||||
| page / pageSize | Integer | **已下线**。回显分页参数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
**接口已下线。** 以下是下线前的请求形态,用来识别调用点:
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/grab-pool/my-claims/vehicle?status=ALL&page=1&pageSize=20
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**接口已下线,现在的实际响应(测试服经网关实测;HTTP 状态码 200,业务码 `code=404`):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: GET /v3/admin/order/grab-pool/my-claims/vehicle",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
**不适用。** 接口已不存在,不论带什么参数、用哪个账号,响应都与上面相同,没有空数据或降级分支。下线前的「空数据」是它唯一的正常响应(200 空页)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
下线前这个接口没有专属错误码。现在唯一的响应就是路由未命中:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: GET /v3/admin/order/grab-pool/my-claims/vehicle",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 下线是纯删除,只删了这个接口,以及只为它存在的 VO、DTO、Mapper 方法、Service 方法和单测。
|
||||
- 房务的 `GET /v3/admin/order/grab-pool/my-claims/hotel`、`GET /v3/admin/order/grab-pool/my-claims/group-batches` 本单没有改动,仍然可以调用。(`my-claims/hotel` 在 #8375 另有安排:dev-v3 `72d8fe310` 已把它标为 `@Deprecated`,接口仍在服务。)
|
||||
- 用车需求响应里的 `claimerId` / `claimerName`(`VehicleRequirementRespVO`)本单没删,车务侧恒为 null。不要用它判断车务归属。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 不要调用 `GET /v3/admin/order/grab-pool/my-claims/vehicle`,也不要重试或做降级兜底:它现在固定返回上面那个响应。
|
||||
- 13_7439 §5 的 kind 入参与 `records[].kind` 作废,不要按它实现「车务我的抢单」页面。
|
||||
- 车务归属用订单详情的「操作车务」(#8342)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本单零数据库变更:没有 Flyway 脚本,表、列、索引都不动。`order_vehicle_requirement` 的 `claimer_id` / `claimer_name` 两列保留。被下线的接口只读不写,下线不影响任何数据。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 调 `GET .../my-claims/vehicle`,不带参数 | 返回上面那个路由未命中响应 |
|
||||
| 带 `status` / `kind` / 分页参数 | 同上,路由层就没命中,不会走到参数校验 |
|
||||
| 调 `GET .../my-claims/hotel` | 照常 200,本单未改动 |
|
||||
| 调 `GET .../my-claims/group-batches` | 本单未改动 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `GET /v3/admin/order/grab-pool/my-claims/vehicle` | 路由存在,对所有账号返回 200 空页 | 路由不存在,返回上面那个响应 |
|
||||
| `kind` 入参 / `records[].kind` | 13_7439 §5 写了,代码未实现 | 作废 |
|
||||
| `MyClaimsQueryReqVO` / `VehicleClaimItemVO` | 存在 | 已删除 |
|
||||
| `my-claims/hotel`、`my-claims/group-batches` | 可调用 | 不变 |
|
||||
| 网关路由配置 | — | 未改动(`/v3/admin/**` 通配) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui `origin/v2.1`(`136973bf`,与远端 tip 一致)搜 `my-claims/vehicle` 0 命中,同一命令能搜到 `my-claims/hotel`(`src/api/housekeeper/grab-pool.js:80`)。测试服 hl-gateway 日志覆盖 2026-09-18 ~ 09-26,该路径只命中 1 次,是后端取证时自己调的,第三方调用 0 次。
|
||||
- **前端要动的**:无。若有未纳入检查的调用点,它会从「200 空页」变成路由未命中;它原本就拿不到任何数据。
|
||||
- **数据影响**:零。
|
||||
- **其它服务**:只改 order-v3,网关、hl-common、fleet 都没动。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 房务 `my-claims/hotel`、`all-claims/hotel`、`hotel-requirements`、`my-claims/group-batches` 都不因本单变化。
|
||||
- `HotelRequirementMapper` 的同名查询方法保留,只删了 `VehicleRequirementMapper` 那一个。
|
||||
- 用车需求的其它接口(提交、放行、打回、派单)不变。
|
||||
- 表结构、Flyway、网关路由零改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署**:`hl-order-service-v3` 两个实例(8086 / 8186)滚动部署分支 `fix/8373-verify-base-359647415`,HEAD `0fbb975bc`。部署日志原文 `[OK] git sync done: branch=fix/8373-verify-base-359647415 HEAD=0fbb975bc`,15:33:30 两个实例都已启动,Nacos 两个实例都 healthy。
|
||||
|
||||
- **构建内容**:`0fbb975bc` = 测试服原来运行的 `359647415` + cherry-pick 本单 `1ac676a85`。两者 `git patch-id --stable` 相同(`934452b6f16c`),即下线改动与 dev-v3 上合入的那份逐行一致。
|
||||
- **为什么不是 dev-v3 最新提交**:dev-v3 最新提交里有 hl-finance 的迁移 `V20260925_127`,它在测试库执行失败(#8362),带着它的 jar 启动时会报 failed migration。本单只删 order-v3 的一个读接口,与这个迁移无交集。
|
||||
- **调用链路**:全部经测试服 nginx 网关入口 `api.test.1814.love:9443`,同一个管理后台账号的 Bearer token。
|
||||
|
||||
### 改前(`359647415`)
|
||||
|
||||
```
|
||||
GET /v3/admin/order/grab-pool/my-claims/vehicle × 6 -> HTTP 200 code=200 records=[] total=0 每次都是空页
|
||||
GET /v3/admin/order/grab-pool/my-claims/hotel × 1 -> HTTP 200 code=200 total=47 阳性对照
|
||||
```
|
||||
|
||||
### 改后(`0fbb975bc`)
|
||||
|
||||
```
|
||||
GET /v3/admin/order/grab-pool/my-claims/vehicle × 8 -> HTTP 200 {"code":404,"message":"接口不存在: GET /v3/admin/order/grab-pool/my-claims/vehicle","data":null,"traceId":null,"success":false}
|
||||
GET /v3/admin/order/grab-pool/my-claims/hotel × 1 -> HTTP 200 code=200 total=47 阳性对照:同 token、同网关,结果与改前相同
|
||||
```
|
||||
|
||||
8 次调用覆盖网关负载均衡下的两个实例,结果全部相同。注意 HTTP 状态码是 200,业务码是 `code=404`,前端判断以 `code` 为准。
|
||||
|
||||
### 合并前精确测试
|
||||
|
||||
`VehicleRequirementAdminControllerTest` 12、`RequirementServiceTest` 318、order-v3 ArchTest 11 个类,合计 `Tests run: 398, Failures: 0, Errors: 0`,BUILD SUCCESS。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8373
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8377
|
||||
- 被作废的交接件:`changelogs-v2/2026-09/13_7439_团期车务地基-用车需求分家-修改接口-管理后台.md` §5(标题下已加订正指针)
|
||||
- 车务归属:#8342「操作车务」
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:wx
|
||||
- 前端(hl-ui 管理后台):mmg
|
||||
@@ -0,0 +1,618 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8375"
|
||||
title: "房务配房列表改版:并入订单列表,新增两个读端点(户级/团期),旧池端点标废弃,菜单与通知链接改向"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "beb6e18b55866f9914811362638b7d3713044d70"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "订单列表新增两个读端点替代房务抢单池;五个旧读端点标 @Deprecated;三个错误码文案改动(去「抢单池」);菜单 101/102 下线;团期通知链接改为订单列表。后端三个 PR(product-v2/#8381、order-v3/#8382、user-service/#8379)已合入 dev-v3 并部署测试服,逐条实测验证通过。前端 2026-09-27 已交付并回写 verified:订单列表重写为表格模式(产品页签全部/核心/定制/团期+状态页签户级七桶/团期三桶+scope 全部/我的+「开始配房」canStartAllocation 唯一依据+?groupBatchId= 团期定位),旧抢单池两页与五个旧读端点调用已删除,写口(claim/transfer/release/takeover)不变仅改入口;house-allocation-orders spec 8 例+房务域 161 例全绿(mmg,beb6e18b,v2.1)。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 房务订单列表改版:新读端点替代抢单池、旧端点标废弃、菜单下线、通知改向(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)/ hl-user-service(端口 8089/8189)
|
||||
> **PR**: #8381(product-v2)/ #8382(order-v3)/ #8379(user-service)
|
||||
> **Issue**: #8375
|
||||
> **日期**: 2026-09-26
|
||||
> **影响范围**: 房务管家 › 订单列表的页签切换、筛选项恢复、「开始配房」入口改向、所有房务角色的可见范围放开、旧页面与菜单下线
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **新增两个读端点**,替代房务抢单池两个旧页面:
|
||||
- `GET /v3/admin/order/house-allocation/households`:户级(核心/定制订单)
|
||||
- `GET /v3/admin/order/house-allocation/group-batches`:团期订单
|
||||
|
||||
2. **五个旧读端点标 `@Deprecated`**,行为不变,但前端改调新端点:
|
||||
- `GET /v3/admin/order/grab-pool/hotel-requirements`
|
||||
- `GET /v3/admin/order/grab-pool/group-batches`
|
||||
- `GET /v3/admin/order/grab-pool/my-claims/hotel`
|
||||
- `GET /v3/admin/order/grab-pool/my-claims/group-batches`
|
||||
- `GET /v3/admin/order/grab-pool/all-claims/hotel`
|
||||
|
||||
3. **权限视图放开**:两个新端点都挂读门 `HouseReadPermission`,不限 `scope=all`;普通房务也能看全员认领情况(旧的 `all-claims/hotel` 仍返回 808092)。
|
||||
|
||||
4. **认领写口不变**:保持现有逻辑,只是入口从池页改到列表行:
|
||||
- 户级:`POST /v3/admin/order/hotel-requirements/{requirementId}/claim`
|
||||
- 团期:`POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim`
|
||||
|
||||
5. **三个错误码文案改动**,去掉"抢单池"页面名:
|
||||
- 808650:改为「团期订单须整团认领后配房,不支持逐户认领 / 转单」
|
||||
- 808612:改为「该团期尚未被房务整团认领」
|
||||
- 808001:改为「该需求已被其他房务认领或状态已变化,请刷新后重试」
|
||||
|
||||
6. **菜单下线**:房务抢单池菜单(菜单 101 与 102)已在 user-service 侧置为 INACTIVE,部署后生效。
|
||||
|
||||
7. **团期通知链接改向**:站内链接由 `/housekeeper/grab-pool-group?groupBatchId=` 改为 `/housekeeper/orders?groupBatchId=`(订单列表团期页签)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
房务原来要在"订单列表"和"房务抢单池"两个页面之间来回切,体验割裂。本次改版将**待配房**的单并入**订单列表**,整合三类订单视图:
|
||||
- 待配房:所有房务都能点「开始配房」认领
|
||||
- 配房中:谁在做,全员可见
|
||||
- 已认领:可按「我的」或「全部」筛选
|
||||
|
||||
新端点按以下规则返回行:
|
||||
- **户级行**(order_main 表,group_batch_id 与 product_batch_id 都为空)
|
||||
- 待配房:`status=PENDING` 且 `claimer_id IS NULL`
|
||||
- 已认领:`status=PENDING` 且 `claimer_id IS NOT NULL`
|
||||
- **团期行**(order_group_batch 表,整团一行)
|
||||
- 待配房:`house_claimer_id IS NULL` 且 `requirement_confirmed=1` 且 `batch_status=RESOURCE_PREPARING`
|
||||
- 已认领:`house_claimer_id IS NOT NULL`(任意阶段)
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 房务配房列表·户级 | GET | `/v3/admin/order/house-allocation/households` | **新增接口** | 户级页签(核心/定制订单),分页返回 |
|
||||
| 2 | 房务配房列表·团期 | GET | `/v3/admin/order/house-allocation/group-batches` | **新增接口** | 团期页签,整团一行 |
|
||||
|
||||
**旧端点标废弃(行为不变)**:`GET /v3/admin/order/grab-pool/hotel-requirements`、`/grab-pool/group-batches`、`/grab-pool/my-claims/hotel`、`/grab-pool/my-claims/group-batches`、`/grab-pool/all-claims/hotel` 加 `@Deprecated`,详见第五章节。
|
||||
|
||||
网关无改动(均在既有 `/v3/admin/order/` 前缀下);新路径已验证可达。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 房务配房列表·户级 `GET /v3/admin/order/house-allocation/households`
|
||||
|
||||
**VO**: `HouseAllocationHouseholdPageReqVO` → `HouseAllocationHouseholdPageRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务管家 › 订单列表的**核心**和**定制**页签,以及**全部**页签的户级数据部分。返回户级订单(order_main),按待配房/已认领分行展示,支持 `scope` 筛选当前用户或全员,按多种条件排序和分页。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| scope | Query | String | 否 | `all` \| `mine`,小写 | 默认 `all`;`mine` 则只返回当前用户认领的行,`pendingClaim` 桶恒为 0 |
|
||||
| status | Query | String | 否 | `pendingClaim` \| `unfinished` \| `claiming` \| `pendingConfirm` \| `confirmed` \| `exception`,或空 | 默认 `unfinished`;空串表示全部;返回对应 house_status 的行 |
|
||||
| productType | Query | String | 否 | `CORE` \| `CUSTOM`,或空 | 空或不传表示全部户级行;不接受 `productType=GROUP`(团期订单走团期端点) |
|
||||
| keyword | Query | String | 否 | `@Size(max=32)` | 模糊匹配:订单号 / 团号 / 客人姓名 / 产品名(OR) |
|
||||
| productName | Query | String | 否 | — | 模糊匹配产品名 |
|
||||
| consultantId | Query | Long | 否 | — | 定制师 ID,精确匹配 |
|
||||
| guestName | Query | String | 否 | — | 客人姓名,模糊匹配 |
|
||||
| departDateFrom | Query | LocalDate | 否 | `yyyy-MM-dd` | 出发日期起 |
|
||||
| departDateTo | Query | LocalDate | 否 | `yyyy-MM-dd` | 出发日期止(含) |
|
||||
| claimedAtFrom | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间起;传了就等于只看已认领的行 |
|
||||
| claimedAtTo | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间止 |
|
||||
| page | Query | Integer | 否 | `@Min(1)` | 默认 1 |
|
||||
| pageSize | Query | Integer | 否 | `@Min(1) @Max(100)` | 默认 20 |
|
||||
| sortBy | Query | String | 否 | `departDate,asc` \| `createTime,desc` \| `claimedAt,desc` | 默认 `departDate,asc`;**无论选哪个,都先按手动加急(manual_urgent DESC, manual_urgent_at DESC)排** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| list | List\<HouseAllocationHouseholdRespVO\> | 分页结果行 |
|
||||
| total | Long | 满足条件的总行数 |
|
||||
| stats | HouseAllocationHouseholdStatsVO | 各页签的计数(与列表共用筛选条件,忽略 status) |
|
||||
|
||||
**行字段详情(HouseAllocationHouseholdRespVO)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | String | requirementId,前端「开始配房」时用此值 |
|
||||
| orderId | String | 订单 ID,进户级详情时用 |
|
||||
| orderNo / teamNo | String | 订单号 / 团号 |
|
||||
| guestName | String | 客人姓名 |
|
||||
| personsDesc | String | 人数描述(如「2成人1儿童」) |
|
||||
| productType | String | 产品类型(CORE/CUSTOM/ROUTE/…) |
|
||||
| productName | String | 产品名称 |
|
||||
| productNo | String | 产品编号;来自 product-v2 批量查询,**取不到时为 null**(见降级响应) |
|
||||
| route | String | 行程描述 |
|
||||
| cities | List\<String\> | 城市列表 |
|
||||
| departDate | LocalDate | 出发日期 |
|
||||
| nights | Integer | 晚数 |
|
||||
| totalAmount | String | 订单金额(后端计算,已序列化为字符串) |
|
||||
| consultantId | String | 定制师 ID |
|
||||
| consultantName | String | 定制师姓名 |
|
||||
| consultantRemark | String | 定制师备注 |
|
||||
| requirementNote | String | 需求备注 |
|
||||
| dispatchRemark | String | 派车备注 |
|
||||
| special | String | 特殊需求 |
|
||||
| requirementVersion | Integer | 需求版本号 |
|
||||
| requirementStatus | String | 对外状态(PENDING/PROCESSING/DONE/…) |
|
||||
| houseStatus | String | **枚举码**:PENDING_CLAIM / CLAIMING / PENDING_FINALIZE / CONFIRMED / EXCEPTION;与旧 VO 的中文名不同 |
|
||||
| houseStatusLabel | String | 中文显示名(「待配房」/「配房中」/「待确认」/「已确认」/「异常」) |
|
||||
| claimerId | String | 当前认领人 ID;待配房行为 null |
|
||||
| claimerName | String | 当前认领人名字;待配房行为 null |
|
||||
| claimedAt | LocalDateTime | 认领时间;待配房行为 null |
|
||||
| isMine | Boolean | 认领人是否当前用户 |
|
||||
| canStartAllocation | Boolean | 前端「开始配房」按钮的**唯一依据**(true 时显示,false 时隐藏);条件:`status=PENDING` 且 `claimerId IS NULL` 且当前用户有写权限 |
|
||||
| manualUrgent | Boolean | 手动加急标记 |
|
||||
| manualUrgentAt | LocalDateTime | 加急时间 |
|
||||
| urgencyLevel | String | 紧急度标签(LOW/MEDIUM/HIGH) |
|
||||
| urgencyLabel | String | 紧急度中文名(「低」/「中」/「高」) |
|
||||
| daysToDepart | Integer | 距出发还有几天(负数表示已出发) |
|
||||
| isRework | Boolean | 是否返工 |
|
||||
| reworkPrevClaimerName | String | 上次认领人名字 |
|
||||
| exceptionCount | Integer | 已认领行:该户有几个未处理异常待办;待配房行为 0 |
|
||||
| todoCount | Integer | 已认领行:该户有几个待办;待配房行为 0 |
|
||||
| primaryAction | HousePrimaryActionVO | 只在 `isMine=true` 时返回;包含转单、释放等操作信息 |
|
||||
| createTime | LocalDateTime | 需求创建时间 |
|
||||
|
||||
**stats 分桶计数(HouseAllocationHouseholdStatsVO)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| pendingClaim | Long | 待配房行数;`scope=mine` 时恒为 0 |
|
||||
| claiming | Long | house_status=CLAIMING 的行数 |
|
||||
| pendingConfirm | Long | house_status=PENDING_FINALIZE 的行数 |
|
||||
| confirmed | Long | house_status=CONFIRMED 的行数 |
|
||||
| exception | Long | house_status=EXCEPTION 或订单有未处理异常待办的行数 |
|
||||
| unfinished | Long | 待配房 + 配房中 + 待确认 + 异常 的行数;与 `status=unfinished` 的查询结果同源 |
|
||||
| all | Long | 待配房 + 已认领 的总行数;与不传 status 的查询结果同源 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/house-allocation/households?scope=all&status=pendingClaim&productType=CORE&page=1&pageSize=20&sortBy=departDate,asc HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer ***
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": "2097250563497385985",
|
||||
"orderId": "770145",
|
||||
"orderNo": "O20260926001",
|
||||
"teamNo": null,
|
||||
"guestName": "张三",
|
||||
"personsDesc": "2成人1儿童",
|
||||
"productType": "CORE",
|
||||
"productName": "日本东京 6 日游",
|
||||
"productNo": "P002301",
|
||||
"route": "东京→富士山→京都",
|
||||
"cities": ["东京", "富士山", "京都"],
|
||||
"departDate": "2026-10-01",
|
||||
"nights": 6,
|
||||
"totalAmount": "8500.00",
|
||||
"consultantId": "1001",
|
||||
"consultantName": "李四",
|
||||
"consultantRemark": "客户有特殊需求",
|
||||
"requirementNote": "希望升级酒店",
|
||||
"dispatchRemark": "已预留",
|
||||
"special": "客户晕车,安排靠窗座位",
|
||||
"requirementVersion": 1,
|
||||
"requirementStatus": "PROCESSING",
|
||||
"houseStatus": "PENDING_CLAIM",
|
||||
"houseStatusLabel": "待配房",
|
||||
"claimerId": null,
|
||||
"claimerName": null,
|
||||
"claimedAt": null,
|
||||
"isMine": false,
|
||||
"canStartAllocation": true,
|
||||
"manualUrgent": true,
|
||||
"manualUrgentAt": "2026-09-25T14:30:00",
|
||||
"urgencyLevel": "HIGH",
|
||||
"urgencyLabel": "高",
|
||||
"daysToDepart": 5,
|
||||
"isRework": false,
|
||||
"reworkPrevClaimerName": null,
|
||||
"exceptionCount": 0,
|
||||
"todoCount": 0,
|
||||
"primaryAction": null,
|
||||
"createTime": "2026-09-20T09:00:00"
|
||||
}
|
||||
],
|
||||
"total": 12,
|
||||
"stats": {
|
||||
"pendingClaim": 12,
|
||||
"claiming": 5,
|
||||
"pendingConfirm": 3,
|
||||
"confirmed": 8,
|
||||
"exception": 1,
|
||||
"unfinished": 21,
|
||||
"all": 28
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 分页无结果时,`list=[]`,`total=0`,`stats` 各字段为 0,`code=200` 不报错:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"list": [],
|
||||
"total": 0,
|
||||
"stats": { "pendingClaim": 0, "claiming": 0, "pendingConfirm": 0, "confirmed": 0, "exception": 0, "unfinished": 0, "all": 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **productNo 取不到时**(product-v2 服务降级或响应缺字段),该字段为 null,其余字段正常返回,不报错。
|
||||
|
||||
- **参数不合法时**(status、productType、sortBy 等与规则不符),HTTP 200,`code=400`(全局 BindException 处理),`message` 是具体字段的校验文案。下面是传 `productType=GROUP` 的实测原文:
|
||||
|
||||
```json
|
||||
{"code":400,"message":"productType 只能是 CORE 或 CUSTOM","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
- **未登录或无房务读权限**(808090):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808090,
|
||||
"message": "未登录或无该操作权限",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 查询条件 `claimedAtFrom/claimedAtTo` 同时传递时,等于隐含过滤 `claimerId IS NOT NULL`(只看已认领的行);与 `status=pendingClaim` 同传时查询结果为空、`code=200`、`stats.pendingClaim=0`,不报错。
|
||||
- 手动加急 `manual_urgent=true` 的行在任何 `status` 页签里都会被提前排列(加急 DESC,加急时间 DESC),无论选何种 sortBy。
|
||||
- **并发认领**:前端的「开始配房」按钮直接调 `POST /v3/admin/order/hotel-requirements/{id}/claim`,若二人同时点同一行,先到者返回 200,后到者返回 808001 并附加新文案「该需求已被其他房务认领或状态已变化,请刷新后重试」,前端刷新列表即可看到当前认领人。
|
||||
- **异常数据(存量)**:列表里个别行的 `orderNo` 或 `productName` 可能为 null(对应主订单已软删除),前端需容忍空值、不做特殊提示,只显示已有字段。
|
||||
- 转单、释放等操作只在 `isMine=true` 的行显示(`primaryAction` 非 null);他人认领的行为只读。
|
||||
- `requirementStatus` 是订单对外状态,`houseStatus` 是该户配房阶段(两个维度独立)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 房务配房列表·团期 `GET /v3/admin/order/house-allocation/group-batches`
|
||||
|
||||
**VO**: `HouseAllocationGroupPageReqVO` → `HouseAllocationGroupPageRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单列表的**团期**页签,整团一行。返回团期订单(order_group_batch),按待配房/已认领分行展示。前端的通知链接 `?groupBatchId=<团ID>` 也以此端点定位落地。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| scope | Query | String | 否 | `all` \| `mine` | 默认 `all`;`mine` 时只返回当前用户认领的团 |
|
||||
| status | Query | String | 否 | `pendingClaim` \| `claimed`,或空 | 空或不传表示两者都要 |
|
||||
| groupBatchId | Query | Long | 否 | — | 精确定位一个团(通知链接用);传了则 1 条或 0 条结果 |
|
||||
| keyword | Query | String | 否 | `@Size(max=32)` | 模糊匹配:团期号 / 产品名(OR) |
|
||||
| productId | Query | Long | 否 | — | 产品 ID,精确匹配 |
|
||||
| batchStatus | Query | String | 否 | `RECRUITING` \| `RESOURCE_PREPARING` \| `MATERIAL_PREPARING` \| `PENDING_DEPARTURE` \| `TRAVELLING` \| `TRIP_FINISHED` \| `REVIEWING` \| `SETTLED` \| `CANCELLED` | 团期阶段,精确匹配 |
|
||||
| departDateFrom | Query | LocalDate | 否 | `yyyy-MM-dd` | 出发日期起 |
|
||||
| departDateTo | Query | LocalDate | 否 | `yyyy-MM-dd` | 出发日期止 |
|
||||
| claimedAtFrom | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间起 |
|
||||
| claimedAtTo | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间止 |
|
||||
| page | Query | Integer | 否 | `@Min(1)` | 默认 1 |
|
||||
| pageSize | Query | Integer | 否 | `@Min(1) @Max(100)` | 默认 20 |
|
||||
| sortBy | Query | String | 否 | `departDate,asc` \| `claimedAt,desc` \| `createTime,desc` | 默认 `departDate,asc` |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| list | List\<HouseAllocationGroupRespVO\> | 分页结果行 |
|
||||
| total | Long | 满足条件的总行数 |
|
||||
| stats | HouseAllocationGroupStatsVO | 三个页签的计数:pendingClaim / claimed / all |
|
||||
|
||||
**行字段详情(HouseAllocationGroupRespVO)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期 ID,前端「开始配房」时用此值 |
|
||||
| batchNo | String | 团期号 |
|
||||
| batchName | String | 团期名称 |
|
||||
| batchLabel | String | 团期标签 |
|
||||
| productId | String | 产品 ID |
|
||||
| productName | String | 产品名称 |
|
||||
| batchStatus | String | 团期阶段码(RECRUITING/RESOURCE_PREPARING/…) |
|
||||
| batchStatusLabel | String | 中文显示名(「招募中」/「资源准备中」/…) |
|
||||
| departDate | LocalDate | 出发日期 |
|
||||
| endDate | LocalDate | 结束日期 |
|
||||
| enrollDeadline | LocalDate | 报名截止日期 |
|
||||
| enrolledRooms | Integer | 已报名房间数 |
|
||||
| enrolledPeople | Integer | 已报名人数 |
|
||||
| activeOrderCount | Integer | 活跃订单数 |
|
||||
| hotelOrderCount | Integer | 需要配房的订单数 |
|
||||
| hotelReady | Integer | 已配房的订单数 |
|
||||
| daysToDepart | Integer | 距出发还有几天 |
|
||||
| urgencyLevel | String | 紧急度标签(LOW/MEDIUM/HIGH) |
|
||||
| urgencyLabel | String | 紧急度中文名 |
|
||||
| requirementConfirmed | Boolean | 团期需求是否已整体确认(仅信息展示,不影响认领) |
|
||||
| houseClaimerId | String | 团级认领人 ID;待配房时为 null |
|
||||
| houseClaimerName | String | 团级认领人名字;待配房时为 null |
|
||||
| houseClaimedAt | LocalDateTime | 认领时间;待配房时为 null |
|
||||
| isMine | Boolean | 认领人是否当前用户 |
|
||||
| canStartAllocation | Boolean | 前端「开始配房」按钮的**唯一依据**;条件:`houseClaimerId IS NULL` 且当前用户有写权限 |
|
||||
| createTime | LocalDateTime | 创建时间 |
|
||||
|
||||
**stats 分桶计数(HouseAllocationGroupStatsVO)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| pendingClaim | Long | 待配房团数(`houseClaimerId IS NULL` 且 `requirementConfirmed=1` 且 `batchStatus=RESOURCE_PREPARING`) |
|
||||
| claimed | Long | 已认领团数(`houseClaimerId IS NOT NULL`,任意阶段) |
|
||||
| all | Long | pendingClaim + claimed 的总数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/house-allocation/group-batches?scope=all&status=pendingClaim&page=1&pageSize=20 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer ***
|
||||
```
|
||||
|
||||
无请求体。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchNo": "G20260926001",
|
||||
"batchName": "日本 10 月团·东京-京都-大阪",
|
||||
"batchLabel": "标准",
|
||||
"productId": "5001",
|
||||
"productName": "日本东京-京都-大阪 8 日游",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusLabel": "资源准备中",
|
||||
"departDate": "2026-10-05",
|
||||
"endDate": "2026-10-12",
|
||||
"enrollDeadline": "2026-09-28",
|
||||
"enrolledRooms": 8,
|
||||
"enrolledPeople": 16,
|
||||
"activeOrderCount": 8,
|
||||
"hotelOrderCount": 8,
|
||||
"hotelReady": 0,
|
||||
"daysToDepart": 9,
|
||||
"urgencyLevel": "HIGH",
|
||||
"urgencyLabel": "高",
|
||||
"requirementConfirmed": false,
|
||||
"houseClaimerId": null,
|
||||
"houseClaimerName": null,
|
||||
"houseClaimedAt": null,
|
||||
"isMine": false,
|
||||
"canStartAllocation": true,
|
||||
"createTime": "2026-09-15T10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 5,
|
||||
"stats": {
|
||||
"pendingClaim": 5,
|
||||
"claimed": 12,
|
||||
"all": 17
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 分页无结果:`list=[]`,`total=0`,`stats` 各字段为 0,`code=200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"list": [],
|
||||
"total": 0,
|
||||
"stats": { "pendingClaim": 0, "claimed": 0, "all": 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 参数不合法时,HTTP 200,`code=400`,`message` 是具体字段的校验文案(结构同户级端点):
|
||||
|
||||
```json
|
||||
{"code":400,"message":"<该字段的校验文案>","data":null,"traceId":null,"success":false}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
- **未登录或无房务读权限**(808090):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808090,
|
||||
"message": "未登录或无该操作权限",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **整团一行**:团期阶段任意时刻只要被认领一次,就会在列表持续出现(`claimed` 页签或混合页签),不会因为进入后续阶段(待出发、出行中等)而消失。
|
||||
- **并发认领**:前端的「开始配房」按钮直接调 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim`。并发场景下:
|
||||
- 团期 claim 端点挂了 `@Idempotent`(5 秒窗口)与 `@Lock4j` 两重防护
|
||||
- 同时提交会被幂等或锁层拦下,返回 **100502**(「整团认领处理中,请勿重复提交」,幂等窗口内),前端提示并刷新列表
|
||||
- 窗口过后别人已抢到会返回 **808652**(「该团期已被其他房务认领」)
|
||||
- **他人认领的团不提供看板入口**:前端对 `isMine=false` 的行不显示「查看看板」入口(若用户直接访问详情页,会得到 808612 或 808613 拒绝);详情页权限不变。
|
||||
- 订单列表的通知参数 `?groupBatchId=<团ID>` 须在团期页签按此参数定位(可配合 filter 或直接查询定位)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 调用方法 | 说明 |
|
||||
|------|---------|------|
|
||||
| ✅ 户级「开始配房」| `POST /v3/admin/order/hotel-requirements/{id}/claim` | 直接调现有写口,行为不变;`canStartAllocation=true` 时显示按钮 |
|
||||
| ✅ 团期「开始配房」| `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim` | 同上;路径保留 grab-pool,因为是现有写口 |
|
||||
| ✅ 查看全员认领情况 | `GET /v3/admin/order/house-allocation/households?scope=all` 或 `/group-batches?scope=all` | **普通房务首次可见**;旧端点 `all-claims/` 仍返回 808092 |
|
||||
| ✅ 刷新列表信号 | 监听 SSE 事件 `grab-pool-changed` | 现有 SSE 通道保留,不新增事件 |
|
||||
| ❌ 调旧端点继续写代码 | `GET /v3/admin/order/grab-pool/hotel-requirements` | @Deprecated,禁用 |
|
||||
| ❌ 团期已被他人认领后再点「开始配房」 | — | 返回 808652「该团期已被其他房务认领」,刷新列表即可看到认领人,不要重试 |
|
||||
|
||||
---
|
||||
|
||||
### 配套变更说明
|
||||
|
||||
#### 错误码文案更新
|
||||
|
||||
随 PR-1 部署,三个错误码文案改动(去掉对「抢单池」页面的引用):
|
||||
|
||||
| 码 | 旧文案 | 新文案 | 触发场景 |
|
||||
|----|--------|--------|---------|
|
||||
| **808650** | "团期订单须整团认领,不支持逐户认领。请到团期抢单池整团认领" | "团期订单须整团认领后配房,不支持逐户认领 / 转单" | 试图逐户认领 / 转单一个团期订单 |
|
||||
| **808612** | "该团期尚未被房务认领,请先到团期抢单池认领" | "该团期尚未被房务整团认领" | 进入他人未认领的团期看板,或操作不满足条件的团期 |
|
||||
| **808001** | "需求已被其他房务抢到" | "该需求已被其他房务认领或状态已变化,请刷新后重试" | 户级并发认领冲突,或状态变化后重复调用 |
|
||||
|
||||
#### 房务抢单池菜单下线
|
||||
|
||||
随 PR-2 部署(Flyway `V20260926_001`):
|
||||
- 菜单 101(「抢单池·普通」,路径 `/housekeeper/grab-pool`)置为 INACTIVE
|
||||
- 菜单 102(「抢单池·团期」,路径 `/housekeeper/grab-pool-group`)置为 INACTIVE
|
||||
|
||||
菜单对所有角色同步生效(按 ACTIVE 状态过滤)。缓存于 user-service 启动完成时自动刷新;若启动日志出现"应用启动后刷新菜单缓存失败",需手工 Redis DEL 对应 key(1 小时 TTL 后自动过期)。
|
||||
|
||||
#### 团期通知链接改向
|
||||
|
||||
随 PR-2 部署(SQL Flyway 更新通知配置),三条团期站内链接由抢单池改指订单列表:
|
||||
|
||||
| 通知事件 | 原链接 | 新链接 | 内容变化 |
|
||||
|---------|--------|--------|---------|
|
||||
| GROUP_BATCH_HOUSE_CLAIMED | `/housekeeper/grab-pool-group?groupBatchId=` | `/housekeeper/orders?groupBatchId=` | 链接改向;内容不再含「抢单池」词语 |
|
||||
| GROUP_BATCH_HOUSE_RELEASED | `/housekeeper/grab-pool-group?groupBatchId=` | `/housekeeper/orders?groupBatchId=` | 同上 |
|
||||
| GROUP_BATCH_REQUIREMENT_CONFIRMED | `/housekeeper/grab-pool-group?groupBatchId=` | `/housekeeper/orders?groupBatchId=` | 同上 |
|
||||
|
||||
#### 旧读端点标废弃
|
||||
|
||||
五个旧端点的 Controller 方法加 `@Deprecated` 注解;Swagger 操作描述末尾补充「(已废弃,改用 /v3/admin/order/house-allocation/…)」。**行为与字段完全不变**,现有消费方在迁移完毕前仍可继续调用,但新代码禁止接入。
|
||||
|
||||
迁移完毕后(todos 与作废页签移至新端点或别处)方开工单删除。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无表结构变更,无 H2 迁移脚本改动。user-service Flyway `V20260926_001` 更新系统菜单与通知配置(非业务表)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 两个新端点都挂读门 `HouseReadPermission`,非房务角色(或未登录)返回 808090;普通房务、组长、超管均可调(不限 `scope=all`),这是与旧 `all-claims/` 最大的权限差异。
|
||||
- 写口(认领、转单、释放)权限不变:写门只放行 ROOM_MANAGER 与 SUPER_ADMIN;房务组长(house_keeper_lead)调写口返回 808091,其他角色返回 808090。所以组长在两个新端点上所有行 `canStartAllocation=false`。
|
||||
- SSE 广播现有四类事件 ADDED / CLAIMED / RELEASED / URGENT 保留,团期认领、释放、接管不补新事件(现状事实:旧池亦不发,属既知设计)。
|
||||
- 他人认领的团期行只读:行操作(转单、释放等)只在 `isMine=true` 时展示;若直接访问团期看板详情,会得 808612/808613 拒绝。
|
||||
- 参数校验失败(如 status 值非法)走全局 `BindException`,HTTP 200,`code=400`(非 400 HTTP 状态)。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **认领、转单、释放、接管的写逻辑**:完全不变,只改入口
|
||||
- **订单详情读权限**:不变
|
||||
- **小程序端**(`/v3/mp/`):无改动
|
||||
- **旧池的五个读端点在存量消费方(todos、作废页签)迁移完毕前**仍可继续调用
|
||||
- **SSE 通道与事件**:保留现有四类,不新增
|
||||
- **子订单状态机、配房流程**:不变
|
||||
- **身份权限种子与角色**:不变(仅房务菜单下线,不涉及角色、权限位、权限码)
|
||||
- **网关配置**:无改动;新路径在现有通配规则 `Path=/v3/admin/**` 下已生效
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:`hl-gateway` / `hl-user-service` / `hl-product-service-v2` / `hl-order-service-v3` 四个服务均为 dev-v3 `12707c57c`(`deploy-status.sh` 四行 ok,2026-09-26 15:47~16:06 滚完)。以下读数都是经网关 `api.test.1814.love`、用真实账号鉴权的实测结果(2026-09-26)。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | 户级「开始配房」:两个普通房务并发认领同一行 | 一个 200,另一个 808001「已被认领或状态已变化」。随后另一人在列表里看到该行:`claimerName` = 认领人,`isMine=false`,`canStartAllocation=false` |
|
||||
| 2 | 普通房务对他人已认领的户级行调认领 / 转交 / 释放 | 分别返回 808001 / 808010 / 808020,行状态不变 |
|
||||
| 3 | 普通房务调户级列表 `scope=all` | code=200;旧端点 `GET /grab-pool/all-claims/hotel` 仍返回 808092(旧权限策略不变) |
|
||||
| 4 | 团期「开始配房」:两个普通房务同时点击 | 5 秒幂等窗口内,后到者 100502;窗口外重试 808652。列表中该团 `houseClaimerName` = 先到者 |
|
||||
| 5 | 房务主管(`house_keeper_lead`)调两个列表 | 均 code=200。全量翻页:户级 107 行、团期 109 行,`canStartAllocation` 全部为 false |
|
||||
| 6 | 房务主管调「开始配房」 | 808091「房务组长为只读监督角色,无权执行该操作」 |
|
||||
| 7 | 非房务角色调两个列表 | 808090「未登录或非房务角色,无权操作」 |
|
||||
| 8 | 户级列表传 `productType=GROUP` | HTTP 200,`{"code":400,"message":"productType 只能是 CORE 或 CUSTOM"}` |
|
||||
| 9 | 团期订单不进户级列表 | 全量翻页户级 107 行,与团期的需求 ID、订单 ID 交集均为 0;阳性对照(已知户级行)在列 |
|
||||
| 10 | 加急行排序 | 打标前第 1、2 页都没有加急行;把原在第 2 页第 10 行的样本标为加急后,它移到第 1 页第 1 行,第 2 页没有加急行(样本已还原) |
|
||||
| 11 | `stats` 与列表同源 | 户级:不筛选 total=107,`productType=CORE` total=105,7 个状态分桶的 `stats` 与按该状态查询的 `total` 逐一相等。团期:不筛选 `stats` 为 45 / 64 / 109(pendingClaim / claimed / all),带关键字为 24 / 53 / 77,均与对应 `total` 相等 |
|
||||
| 12 | `scope=mine` 与 `status=pendingClaim` 同传 | code=200,空页 |
|
||||
| 13 | 整团认领后的站内信 | 6 条 CLAIMED 记录的链接为 `/housekeeper/orders?groupBatchId=<团期ID>`,正文不含「抢单池」;用该 `groupBatchId` 查团期列表,total=1 |
|
||||
| 14 | 5 个旧端点 | 均 code=200,响应结构不变;Swagger 中 `deprecated=true` |
|
||||
| 15 | 菜单 | 普通房务 ×2、房务主管、超管共 4 个账号,当前菜单树里都没有菜单 101 / 102 |
|
||||
| 16 | 户级列表 `productNo` | 抽 3 行,与库中产品编号一致 |
|
||||
| 17 | 关联订单已软删的存量需求行 | 该行 `orderNo` / `productName` / `productNo` 为 null,列表 code=200 照常返回 |
|
||||
|
||||
**本次未在测试服触发、只按代码契约给出的**:808612 / 808613(看他人认领的团期看板);`productNo` 取数失败时的降级(Feign fallback 置 null)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- **Issue**: [#8375](https://git.1814.love/wx/HL/issues/8375)
|
||||
- **PR**:
|
||||
- [#8381](https://git.1814.love/wx/HL/pulls/8381)(product-v2)
|
||||
- [#8382](https://git.1814.love/wx/HL/pulls/8382)(order-v3)
|
||||
- [#8379](https://git.1814.love/wx/HL/pulls/8379)(user-service)
|
||||
- **涉及表结构**: 无
|
||||
- **数据库变更**: user-service Flyway `V20260926_001`(菜单下线 + 通知链接改向)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8375](https://git.1814.love/wx/HL/issues/8375)
|
||||
- **合并提交**:
|
||||
- product-v2: [#8381](https://git.1814.love/wx/HL/pulls/8381) @ commit d7a98f13f
|
||||
- order-v3: [#8382](https://git.1814.love/wx/HL/pulls/8382) @ commit 72d8fe310
|
||||
- user-service: [#8379](https://git.1814.love/wx/HL/pulls/8379) @ commit 12707c57c
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人(hl-ui)**: @mmg
|
||||
@@ -0,0 +1,373 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8385"
|
||||
title: "团期订房计划建守卫:超管未认领团上禁建(808612),释放文案改为可操作指引"
|
||||
consumer: "admin"
|
||||
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: "超管在未被房务整团认领的团期上新建订房计划时返回 808612(复用已有码);释放端点的 808660 文案改为可操作的指引(删除计划、联系超管接管)。后端改动已合入 dev-v3 并部署测试服,808612 新守卫与 808660 新文案均已网关实测。前端实证(mmg 2026-09-27):room-plans 写口错误码拦截器透 message,808612/808660 常量已存在新文案自动上屏;超管无未认领团直接排房入口(看板仅 isMine 走团级接管),零改动 not_required。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:团期订房计划守卫与释放指引改进(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8395
|
||||
**Issue**: #8385
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **新增入口守卫**:超管(SUPER_ADMIN)在**未被房务认领的团期**上调用 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans` 时,返回 **808612**「该团期尚未被房务整团认领」,不落库。
|
||||
- 背景:超管本可在无人认领的团上直接建计划,导致"团没人管、但计划和库存都在"的状态。释放时房务因为 808660 无法释放,只剩逐条删计划这一条路。
|
||||
- 约束:超管要在团上排房,必须先 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/takeover` 指派给某个房务,再建计划。
|
||||
|
||||
2. **改进释放文案**:端点 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/release` 的错误码 808660 文案改为:
|
||||
「该团仍有 {0} 条未取消的订房计划,无法释放:请先逐条删除订房计划后再释放;如需更换认领房务请联系超管接管」
|
||||
- 改前文案提到的「走接管」对房务不可用(接管只有超管能做)。改后提供两条可行出口:删除计划(房务自助)/ 联系超管接管(超管权限)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期整团认领的唯一真实指针是 `order_group_batch.house_claimer_id`。超管出于"人离职、团转手"的清理需要被允许越过认领校验,但在一个**没人认领、也不需要清理**的团上直接建计划,不属于这类处置。
|
||||
|
||||
释放时的 808660 守卫堵住了"有计划+无认领人"的释放口,文案则指向一个房务无法执行的操作(接管),导致房务被无谓地卡住。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 提交订房计划 | POST | `/v3/admin/house/group-batches/{groupBatchId}/room-plans` | 新增入口守卫 | 超管未认领团上返回 808612 |
|
||||
| 2 | 释放认领 | POST | `/v3/admin/order/grab-pool/group-batches/{groupBatchId}/release` | 错误码文案改进 | 808660 文案改为可操作指引 |
|
||||
|
||||
网关无改动(既有 `/v3/admin/house/` 与 `/v3/admin/order/` 前缀均可达);返回码、触发条件、已认领团的行为全部不变。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 提交订房计划 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans`
|
||||
|
||||
**VO**: `GroupBatchRoomPlanSaveReqVO → Result<List<GroupBatchRoomPlanRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务或超管在团期上提交多条订房计划(房型、房数、价格、结算方式、库存扣减等)。超管在未认领的团上调用时新增拒绝,改后只能通过先 takeover 指派给房务的方式排房。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | — | 团期 ID |
|
||||
| items | Body | List | 是 | ≤200 条 | 订房计划行 |
|
||||
| items[].stayDate | Body | LocalDate | 是 | 格式 `yyyy-MM-dd` | 入住日期 |
|
||||
| items[].hotelId | Body | Long | 是 | — | 酒店 ID |
|
||||
| items[].roomTypeId | Body | Long | 是 | — | 房型 ID |
|
||||
| items[].roomCount | Body | Integer | 是 | ≥1 | 房间数 |
|
||||
| items[].roomCategory | Body | String | 否 | ≤32 | 房型大类 code;服务端以 resource 权威值覆盖,仅用于前端回显 |
|
||||
| items[].protoPrice | Body | BigDecimal | 否 | ≥0 | 协议价快照;不传按「日历价 → 酒店协议价」兜底 |
|
||||
| items[].settlementPrice | Body | BigDecimal | 否 | ≥0 | 结算价快照;不传按「日历价 → 协议价」兜底 |
|
||||
| items[].settleType | Body | String | 否 | `cash` \| `sign` \| `company` | 结算方式;不传取酒店资源配置 |
|
||||
| items[].deductInventory | Body | Boolean | 否 | 默认 true | 是否扣库存 |
|
||||
| items[].remark | Body | String | 否 | ≤512 | 备注 |
|
||||
| items[].version | Body | Integer | 否 | — | 乐观锁版本;本端点(批量新建)不校验,只有单行修改端点必填并校验 |
|
||||
| items[].replaceReason | Body | String | 否 | ≤256 | 替换原因;本端点不使用,仅单行修改端点在语义为"删旧建新"时使用 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| planId | String | 计划行 ID(雪花 ID,序列化为字符串) |
|
||||
| groupBatchId | String | 团期 ID(雪花 ID,序列化为字符串) |
|
||||
| stayDate | LocalDate | 入住日期 |
|
||||
| hotelId / hotelName | String / String | 酒店 ID(序列化为字符串)及名称 |
|
||||
| roomTypeId / roomTypeName | String / String | 房型 ID(序列化为字符串)及名称 |
|
||||
| roomCount | Integer | 房间数 |
|
||||
| protoPrice | String | 协议价快照,可为 null(BigDecimal,序列化为字符串) |
|
||||
| settlementPrice | String | 结算价快照,可为 null(BigDecimal,序列化为字符串) |
|
||||
| settleType | String | 结算方式快照,可为 null |
|
||||
| planStatus | String | 状态码(PENDING / CONFIRMED) |
|
||||
| version | Integer | 乐观锁版本号 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/house/group-batches/2099918391610314754/room-plans HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"stayDate": "2026-10-01",
|
||||
"hotelId": 1001,
|
||||
"roomTypeId": 5001,
|
||||
"roomCount": 2,
|
||||
"settlementPrice": 450.00,
|
||||
"deductInventory": true,
|
||||
"remark": "标准间"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功(已认领团)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"planId": "2103794613192208385",
|
||||
"groupBatchId": "2099918391610314754",
|
||||
"stayDate": "2026-10-01",
|
||||
"hotelId": "1001",
|
||||
"hotelName": "丽思卡尔顿",
|
||||
"roomTypeId": "5001",
|
||||
"roomTypeName": "豪华标间",
|
||||
"roomCount": 2,
|
||||
"settlementPrice": "450.00",
|
||||
"planStatus": "PENDING",
|
||||
"version": 1,
|
||||
"createTime": "2026-09-26 14:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**失败(未认领团,超管)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808612,
|
||||
"message": "该团期尚未被房务整团认领",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 参数校验失败(日期格式错、房间数 ≤0):HTTP 200,`code=400`,message 含具体字段文案;不落库。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808612,
|
||||
"message": "该团期尚未被房务整团认领",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**其他错误码**:
|
||||
- **808090**:未登录或非房务角色,无权操作。
|
||||
- **808091**:房务组长为只读监督角色,无权执行该操作(HOUSE_KEEPER_LEAD)。
|
||||
- **808613**:该团期由其他房务认领,无权操作。
|
||||
- **808600**:团期当前阶段不允许修改订房计划(订房计划仅在配置阶段可改)。
|
||||
- **808614**:该入住日在酒店的该房型上已有订房计划,请改用修改单行。
|
||||
- **808611**:团期出发日或结束日缺失,无法录入订房计划。
|
||||
- **808602**:入住日不在团期出行区间内。
|
||||
- **808603**:订房间数必须大于 0。
|
||||
- **808691**:无法确定房型大类(取权威房型数据失败),请稍后重试。
|
||||
- **808112**:房型不属于该酒店。
|
||||
- **100502**:3 秒幂等窗口内重复提交,「订房计划提交处理中,请勿重复提交」。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **整团认领**:认领人本人或超管可提交;非认领房务提交返回 808613;团未认领(`house_claimer_id IS NULL`)时所有人(含超管)返回 808612。
|
||||
- **超管排房出口**:超管要在未认领的团上排房,必须先 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/takeover` 指派给房务,再来提交计划。
|
||||
- **库存扣减**:`deductInventory=true` 时,CONFIRMED 状态的计划会扣掉 `resource_hotel_room_inventory` 对应房型该日的可用房数;PENDING 阶段不扣。
|
||||
- **日期范围**:入住日期必须在团期的出发日期与结束日期之间。
|
||||
|
||||
---
|
||||
|
||||
### 2. 释放认领 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/release`
|
||||
|
||||
**VO**: `HouseGroupReleaseReqVO → Result<Void>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务释放所有团期认领,团回到抢单池状态(`house_claimer_id → NULL`)。房务有订房计划待处理时返回 808660,文案告知删除计划或联系超管接管两条出口。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | 是 | — | 团期 ID |
|
||||
| reason | Body | String | 否 | ≤200 字 | 释放原因;超管必填且不少于 10 字 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | null | 成功返回 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/grab-pool/group-batches/2099918391610314754/release HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <room_manager token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"reason": "已完成配房"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**失败(有计划待删)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808660,
|
||||
"message": "该团仍有 4 条未取消的订房计划,无法释放:请先逐条删除订房计划后再释放;如需更换认领房务请联系超管接管",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808660,
|
||||
"message": "该团仍有 4 条未取消的订房计划,无法释放:请先逐条删除订房计划后再释放;如需更换认领房务请联系超管接管",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**其他错误码**:
|
||||
- **808655**:该团期不属于当前房务,无法释放;非认领人释放时触发(超管不受此限),CAS 并发失败(读到时归本人、提交时已被超管释放/接管)也报此码。
|
||||
- **808656**:该团期尚未被认领,无需释放。
|
||||
- **808657**:超管操作原因长度不足 10 字(房务释放时 reason 可为空)。
|
||||
- **808090**:未登录或非房务角色,无权操作。
|
||||
- **808091**:房务组长为只读监督角色,无权执行该操作(HOUSE_KEEPER_LEAD)。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **释放前置**:须先删光全部未取消的订房计划(PENDING + CONFIRMED)才能释放;所有角色(含超管)受 808660 约束,release 本身不碰计划行。
|
||||
- **幂等**:幂等键按团(`groupBatchId`),窗口 5 秒;窗口内重复提交返回 100502「整团释放处理中,请勿重复提交」。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 调用方法 | 说明 |
|
||||
|------|---------|------|
|
||||
| ✅ 超管先排房再指派 | 先 `POST /v3/admin/order/grab-pool/group-batches/{id}/takeover` 指派给房务 → 再 `POST .../room-plans` 提交 | takeover 让团被指定房务认领,之后超管仍可在上面操作 |
|
||||
| ✅ 房务提交计划 | `POST /v3/admin/house/group-batches/{id}/room-plans` | 认领的房务可随时提交,RESOURCE_PREPARING 阶段有效 |
|
||||
| ✅ 房务释放有计划 | 先 `DELETE /v3/admin/house/group-batches/{id}/room-plans/{planId}` 逐条删 → 再 `POST .../release` | 删除由 `HouseGroupBatchClaimGuard` 控制,认领人可删(含他人建的),释放成功返回 200 |
|
||||
| ❌ 超管在未认领团直接建计划 | — | 返回 808612,需先 takeover 指派 |
|
||||
| ❌ 有计划待删时释放 | — | 返回 808660,不释放;先删后释 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无表结构变更、无 Flyway 迁移。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 超管 POST room-plans on 未认领团 | 返回 808612,`group_batch_room_plan` 零新增 |
|
||||
| 超管 takeover 后 POST room-plans | 返回 200,计划行正常落库 |
|
||||
| 房务释放、仍有 PENDING 计划 | 返回 808660,计数含 PENDING |
|
||||
| 房务删掉全部计划、再释放 | 返回 200,团回到池里 |
|
||||
| 超管释放、仍有计划 | 同样返回 808660(不分角色) |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 超管在未认领团上 POST room-plans | 返回 200,计划落库,团仍无主 | 返回 808612,不落库 |
|
||||
| 释放时有计划、808660 文案 | 「无法释放(请先处理订房计划或走接管)」 | 「无法释放:请先逐条删除订房计划后再释放;如需更换认领房务请联系超管接管」 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui 在 `origin/v2.1` 上房务页面无调用 `takeover`(该接口仍在,超管直接用也可以);超管排房流程可能需要调整为"先指派再排"。
|
||||
- **前端要动的**:房务页面若有"直接排房"的超管入口,需提示"未认领团先指派再排";释放时若遇到 808660,按改后文案提示房务删除计划。
|
||||
- **数据影响**:无;本单仅加守卫,不改历史数据。
|
||||
- **其它服务**:product-v2、fleet、user-service 无改动。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 已认领团的超管操作(确认、分房、delete 等)全部不动。
|
||||
- 更新/删除/确认计划的守卫不动(仍走 `assertWritableByCurrentUser`,超管仍可清理)。
|
||||
- 其他团期相关接口(认领、接管、整团确认等)无改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | 超管 POST 未认领团(RESOURCE_PREPARING) | code 808612,计划行零新增 |
|
||||
| 2 | 该团由超管 takeover 给某房务后,超管再 POST | code 200 |
|
||||
| 3 | 超管 DELETE 上一步新增的计划行 | code 200,`releasedLogId=null` |
|
||||
| 4 | 该房务 release | code 200,团回到无主 |
|
||||
| 5 | 另一认领房务逐条 DELETE 自己团的 4 条 PENDING 计划 | 全部 200(`releasedLogId=null`) |
|
||||
| 6 | 上一步之后 release | code 200 |
|
||||
| 7 | 超管对他人认领团的存量计划行 DELETE(62 行) | 全部 200 |
|
||||
| 8 | 认领房务在团上挂 1 条 PENDING 计划时 release | code 808660,message「该团仍有 1 条未取消的订房计划,无法释放:请先逐条删除订房计划后再释放;如需更换认领房务请联系超管接管」,与源码逐字一致 |
|
||||
| 9 | 删掉该计划后再 release | code 200,团回到无主 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8385
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8395
|
||||
- takeover API:`POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/takeover`
|
||||
- 同批单号:#8386、#8387、#8388、#8389、#8390(房务接口审计批次)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,331 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8386"
|
||||
title: "住宿需求驳回:加房务角色门与认领人校验;车务驳回接口下线"
|
||||
consumer: "admin"
|
||||
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: "住宿需求供应方驳回新增房务角色门(808090/808091)与认领人校验(808110/808116,超管豁免);车务驳回接口删除,调用返回 HTTP 200 + code 404。已在测试环境网关实测各角色分支与已删除端点响应。前端实证(mmg 2026-09-27):车务驳回口零调用;住宿驳回按钮 enabled/disabledReason 由后端 canRejectRequirement 驱动,新码拦截器透 message,零改动 not_required。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:住宿需求驳回守卫加固与车务驳回下线(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8397
|
||||
**Issue**: #8386
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **住宿需求供应方驳回** `POST /v3/admin/order/{id}/hotel-requirement/supplier-reject`:
|
||||
- 新增房务角色门:`@HouseWriteGuarded`,非房务角色返回 **808090**,房务组长返回 **808091**。
|
||||
- 新增认领人校验:需求必须被当前操作人认领,否则返回 **808110**(非本人)/ **808116**(未认领,仅超管可驳)。
|
||||
- 超管可绕过认领限制(跳过本人限制,但仍受角色门约束)。
|
||||
|
||||
2. **车务需求供应方驳回** `POST /v3/admin/order/{id}/vehicle-requirement/supplier-reject` **已下线**:
|
||||
- 端点删除,调用返回 404。
|
||||
- hl-ui 全部远程 ref 零调用,车务侧无此功能。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
### 住宿驳回
|
||||
|
||||
API-SPEC §2.8 与 CHANGELOG v6.3.8 均明文「新增房务/超管可用的住宿需求驳回端点」,但实现漏了角色门和认领人校验:
|
||||
- 任意 ADMIN token 的后台账号(包括车控、定制师)都能驳回任意订单的需求。
|
||||
- 测试服已实证车控账号(VEHICLE_MANAGER)成功打回住宿需求两次。
|
||||
- 同类操作(转单、释放、提交配房)都带认领人校验,驳回不带属于遗漏。
|
||||
- 打回副作用重:需求行终态失活、flow 回退、定制师开返工待办 + 站内信。
|
||||
|
||||
### 车务驳回
|
||||
|
||||
车务从无"抢单 → 认领"的流程,用车需求也从不写入抢单人:整个车务侧"我的接单"接口(#8373)因此下线。供应方驳回同样是"假接口"——没有实际用途、hl-ui 零调用。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 住宿需求供应方驳回 | POST | `/v3/admin/order/{id}/hotel-requirement/supplier-reject` | 修改接口 | 新增角色门(808090/808091)与认领人校验(808110/808116) |
|
||||
| 2 | 车务需求供应方驳回 | POST | `/v3/admin/order/{id}/vehicle-requirement/supplier-reject` | 删除接口 | 端点已删除,调用返回 404 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 住宿需求供应方驳回 `POST /v3/admin/order/{id}/hotel-requirement/supplier-reject`
|
||||
|
||||
**VO**: `RejectReqVO → Result<Void>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务(ROOM_MANAGER)或超管在配房面板「驳回需求」,把已认领的住宿需求打回给定制师,触发返工。仅认领人或超管可操作;非房务角色返回权限错误。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | 是 | — | 订单 ID |
|
||||
| returnRemark | Body | String | 是 | `@NotBlank`,≤500 字 | 驳回原因,不能为空 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | null | 成功返回 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/770145/hotel-requirement/supplier-reject HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <room_manager token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"returnRemark": "房型不符,请调整"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**失败(非认领人)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808110,
|
||||
"message": "需求不属于当前用户",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**失败(无房务权限)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808090,
|
||||
"message": "未登录或非房务角色,无权操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- `returnRemark` 缺失或为空白:`@Valid` 校验不通过,HTTP 200 + `code: 400` + 具体字段错误信息(不进入业务逻辑)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "打回/驳回备注不能为空",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**校验顺序**(前一道不过不会走到后一道):写门(808090/808091)→ 参数校验(400)→ 582031(无生效需求行)→ 582083(需求状态不允许此操作)→ 认领人校验(808116/808110,超管豁免)→ 582086(已有生效配房不可驳回)→ 582083(并发 CAS 失败)。
|
||||
|
||||
**其他错误码**:
|
||||
- **808090**:未登录或非房务角色,无权操作。
|
||||
- **808091**:房务组长为只读监督角色,无权执行该操作。
|
||||
- **582031**:订单无有效需求行。
|
||||
- **582083**:需求状态不允许此操作,请检查当前状态(团期订单只放行 PENDING/PROCESSING;非团期订单放行 PENDING/PROCESSING/PENDING_REVIEW;并发 CAS 失败同样报此码)。
|
||||
- **808116**:订单未抢单, 请先抢单再配房(需求未被认领,非超管不可驳)。
|
||||
- **808110**:需求不属于当前用户(已被他人认领,非超管不可驳)。
|
||||
- **582086**:该需求已有配房记录, 不能驳回, 请走替换/修改配房。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **认领限制**:需求 `claimer_id` 必须等于当前操作人,或当前角色为 SUPER_ADMIN。房务组长(house_keeper_lead)无法驳回,返回 808091。
|
||||
- **超管豁免**:SUPER_ADMIN 绕过认领人限制(不受 808110/808116 约束),但仍受角色门约束(必须先过 `@HouseWriteGuarded`)。
|
||||
- **副作用**:驳回目标状态按订单类型区分——核心/定制订单需求驳回后进 `REJECTED_TO_CONSULTANT`,团期订单进 `REJECTED_TO_ADMIN`;触发返工待办与站内信。
|
||||
- **转单后生效**:若需求在驳回前被转单给其他房务,原持有人的驳回请求返回 808110(当前 `claimer_id` 已是新认领人)。
|
||||
- **已有配房记录不可驳回**:需求已生成生效配房(`hasActiveHotelAssignmentsByOrder` 为真)时返回 582086,须走替换/修改配房而非驳回。
|
||||
|
||||
---
|
||||
|
||||
### 2. 车务需求供应方驳回 `POST /v3/admin/order/{id}/vehicle-requirement/supplier-reject`
|
||||
|
||||
**VO**: `已删除`
|
||||
|
||||
**状态:已删除**
|
||||
|
||||
#### 使用场景
|
||||
|
||||
该接口已删除,不可用。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 已删除 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | — | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/770145/vehicle-requirement/supplier-reject HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
HTTP 状态码 **200**(非 404),业务错误码 404:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/770145/vehicle-requirement/supplier-reject",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用(路由已删)。前端应只判 `code`,不依赖 HTTP 状态码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/770145/vehicle-requirement/supplier-reject",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 接口已删除,路由不存在;调用返回 HTTP 200 + `code: 404`(不是 HTTP 404),前端只判 `code`。
|
||||
- hl-ui 全部远程 ref 零调用;测试网关 nginx 日志 2026-09-12~09-26 窗口内该路径 19 次调用全部来自脚本(Python-urllib / curl / 空 UA),零浏览器 UA。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 调用方法 | 说明 |
|
||||
|------|---------|------|
|
||||
| ✅ 认领人驳回 | `POST /v3/admin/order/{orderId}/hotel-requirement/supplier-reject` | 该订单的认领房务直接调,返回 200;定制师开返工待办 |
|
||||
| ✅ 超管代驳 | 同上(SUPER_ADMIN 身份)| 超管跳过认领限制,但仍需房务角色权限 |
|
||||
| ✅ 订单详情按钮 | 查看 `actions.canRejectRequirement` | enabled 取决于 write 权限 + 认领人判定;false 时显示 disabledReason |
|
||||
| ❌ 非认领人驳回 | — | 返回 808110,需求 status 不变 |
|
||||
| ❌ 车务驳回 | — | 端点已删除,返回 HTTP 200 + code 404 |
|
||||
| ❌ 定制师驳回 | — | 返回 808090 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无表结构变更、无 Flyway。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 认领人驳回 | 返回 200,需求进 REJECTED_TO_CONSULTANT 或 REJECTED_TO_ADMIN |
|
||||
| 非认领人驳回 | 返回 808110,需求 status 不变 |
|
||||
| 未认领需求 + 房务驳回 | 返回 808116,非超管不可驳 |
|
||||
| 超管驳回未认领 | 返回 200,驳回处理 |
|
||||
| 转单中驳回(原认领人) | 返回 808110(新认领人接管后视为他人所有) |
|
||||
| 车务驳回 | 返回 HTTP 200 + code 404,无新增副作用 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 车控账号调住宿驳回 | 返回 200,需求打回 | 返回 808090,需求 status 不变 |
|
||||
| 非认领人驳回 | 返回 200,需求打回 | 返回 808110,status 不变 |
|
||||
| 未认领需求 | 任意房务可驳 | 仅超管可驳,返回 808116 |
|
||||
| 车务驳回 | 路由存在,可调用 | 路由删除,返回 HTTP 200 + code 404 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui 房务驳回按钮的 `enabled` 判定由 `canRejectRequirement` 决定(已包含写权限与认领人两个维度),驳回失败收 808090/808091/808110/808116 四个新码。
|
||||
- **前端要动的**:
|
||||
- 驳回按钮的 disabled 文案区分「无房务权限」(808090/808091)与「非本人认领」(808110)。
|
||||
- 处理 808116「未认领,仅超管可驳」的提示(普通房务不应遇到,因为按钮本身 disabled)。
|
||||
- 车务不再有驳回端点,移除 vehicle-requirement/supplier-reject 调用。
|
||||
- **数据影响**:无;仅加守卫,不改历史。
|
||||
- **其它服务**:fleet 的 `rejectVehicleRequirementFromFleet` 独立存在,本单不动。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 团期管理员的 dispatch/reject(走独立 `GroupBatchPermissionGuard`),无改动。
|
||||
- 转单、释放、提交配房等其他配房写操作,无改动。
|
||||
- 车务用车需求的其他接口(提交、放行、派单),无改动。
|
||||
- 整团认领释放等认领链路,无改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | VEHICLE_MANAGER 调住宿 supplier-reject | code 808090,需求状态不变 |
|
||||
| 2 | house_keeper_lead 调住宿 supplier-reject | code 808091,需求状态不变 |
|
||||
| 3 | 非认领 ROOM_MANAGER(需求已被他人认领)驳回 | code 808110,需求状态不变 |
|
||||
| 4 | ROOM_MANAGER 对未认领需求驳回 | code 808116,需求状态不变 |
|
||||
| 5 | 超管打回 / 认领人打回自己认领的需求 | 成功,核心订单需求进 REJECTED_TO_CONSULTANT |
|
||||
| 6 | 车务 `POST /v3/admin/order/{id}/vehicle-requirement/supplier-reject` | HTTP 200 + code 404「接口不存在: POST <URL>」 |
|
||||
|
||||
调用来源:测试网关 nginx 日志 2026-09-12~09-26 窗口内车务 supplier-reject 共 19 次,全部是脚本(Python-urllib / curl / 空 UA),零浏览器 UA;hl-ui 全部远程 ref 零调用。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8386
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8397
|
||||
- 相关单号:#8373(车务我的接单下线)、#8388(回执认领人校验同口径)
|
||||
- API-SPEC:`docs/order-v3/api/API-SPEC-HOUSE-V1.1.html` §2.8
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8387"
|
||||
title: "订单房间分配:下线旧版三接口 GET/POST/PUT /v3/admin/order/{id}/room"
|
||||
consumer: "admin"
|
||||
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: "订单侧旧版房间分配三个端点(GET list、POST add、PUT edit)已下线。API-SPEC 已标旧版,房务配房走 house 域 §2.5 接口。hl-ui 零调用。前端实证(mmg 2026-09-27):被删三口零调用(仅 /order/{id}/room-info/check-diff 不同路径),not_required 复核确认。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:下线订单房间分配旧版接口(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8398
|
||||
**Issue**: #8387
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
三个接口已删除,路由不存在:
|
||||
- `GET /v3/admin/order/{id}/room`:查询订单房间分配列表(旧版)
|
||||
- `POST /v3/admin/order/{id}/room`:新增房间分配(旧版)
|
||||
- `PUT /v3/admin/order/{id}/room/{roomAssignmentId}`:编辑房间分配(旧版)
|
||||
|
||||
调用返回 HTTP 200 + `code: 404`(不是 HTTP 404)。房务配房改用 house 域接口:新增/清空配房走 `POST/DELETE /v3/admin/order/hotel-requirements/{requirementId}/assignments` 等 API-SPEC-HOUSE §2.2~§2.4c 写口;查询订单房间用 `GET /v3/admin/order/orders/{orderId}/rooms`(§2.5)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
订单侧房间分配(`order_room_assignment` 表)是房务配房的补充记录,实现早于 house 域统一接口。API-SPEC §8 已标为"旧版",注释指向 house 域 §2.5 为正式接口。
|
||||
|
||||
hl-ui 全部远程 ref 无调用;房务页面已改用 house 域配房端点。后台读写入口全删,仅保留 internal bundle 读——house 域 `RoomAssignmentVO` 与 hl-mp-service 的 C 端聚合仍需 `order_room_assignment` 表数据,故表与内部读逻辑保留,仅删对外读写接口。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 查询房间分配列表(旧) | GET | `/v3/admin/order/{id}/room` | 删除 | 路由已删,任何调用返回 404 |
|
||||
| 2 | 新增房间分配(旧) | POST | `/v3/admin/order/{id}/room` | 删除 | 路由已删,返回 404 |
|
||||
| 3 | 编辑房间分配(旧) | PUT | `/v3/admin/order/{id}/room/{roomAssignmentId}` | 删除 | 路由已删,返回 404 |
|
||||
|
||||
房务配房改用 house 域接口(API-SPEC-HOUSE):提交配房 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments`(§2.2)、清空配房 `DELETE .../assignments`(§2.4b/§2.4c)、按天确认 `POST .../assignments/days/{dayNumber}/confirm`(§2.3)、调整入住 `PUT .../assignments/{id}/placement`(§2.3b);查询订单房间用 `GET /v3/admin/order/orders/{orderId}/rooms`(§2.5)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 房间分配查询 `GET /v3/admin/order/{id}/room`
|
||||
|
||||
**VO**: `已删除`
|
||||
|
||||
**状态**:已删除。路由不存在,返回 404。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
该接口已删除。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 已删除 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | — | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/770145/room HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
HTTP 状态码 **200**(非 404),业务错误码 404:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: GET /v3/admin/order/770145/room",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用。前端应只判 `code`,不依赖 HTTP 状态码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: GET /v3/admin/order/770145/room",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 接口已删除,路由不存在;调用返回 HTTP 200 + `code: 404`。
|
||||
- 阳性对照:同 Controller 下 `GET /v3/admin/order/{id}` 正常返回 200,证明是该路由本身被删,不是订单不存在。
|
||||
- `order_room_assignment` 表数据仍可由服务内部通过 `internal/order/orders/{orderId}/assignments` 读取(该 internal 端点经网关直接拒绝「接口不可访问」,只服务间可达,不对前端暴露)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 房间分配新增 `POST /v3/admin/order/{id}/room`
|
||||
|
||||
**VO**: `已删除`
|
||||
|
||||
**状态**:已删除。路由不存在,返回 404。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
该接口已删除。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 已删除 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | — | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/770145/room HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
HTTP 状态码 **200**(非 404),业务错误码 404:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/770145/room",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用。前端应只判 `code`,不依赖 HTTP 状态码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/770145/room",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 路由已删除;调用返回 HTTP 200 + `code: 404`。
|
||||
- 新增房间分配改走 house 域 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments`(API-SPEC-HOUSE §2.2)。
|
||||
|
||||
---
|
||||
|
||||
### 3. 房间分配编辑 `PUT /v3/admin/order/{id}/room/{roomAssignmentId}`
|
||||
|
||||
**VO**: `已删除`
|
||||
|
||||
**状态**:已删除。路由不存在,返回 404。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
该接口已删除。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 已删除 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | — | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/order/770145/room/123456 HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
HTTP 状态码 **200**(非 404),业务错误码 404:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: PUT /v3/admin/order/770145/room/123456",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用。前端应只判 `code`,不依赖 HTTP 状态码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: PUT /v3/admin/order/770145/room/123456",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 路由已删除;调用返回 HTTP 200 + `code: 404`。
|
||||
- 编辑房间分配改走 house 域 `PUT /v3/admin/order/hotel-requirements/{requirementId}/assignments/{id}/placement`(API-SPEC-HOUSE §2.3b)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 查看订单的房间 | `GET /v3/admin/order/{orderId}/room` | 调用返回 HTTP 200 + code 404;改用 `GET /v3/admin/order/orders/{orderId}/rooms`(API-SPEC-HOUSE §2.5) |
|
||||
| 新增房间 | `POST /v3/admin/order/{orderId}/room` | 路由已删,不可用;改用 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments`(§2.2) |
|
||||
| 修改房间 | `PUT /v3/admin/order/{orderId}/room/{id}` | 路由已删,不可用;改用 `PUT /v3/admin/order/hotel-requirements/{requirementId}/assignments/{id}/placement`(§2.3b) |
|
||||
|
||||
C 端房型统计(`GET /mp/order/{orderId}/hotels`)的 `roomTypeStats` 仍从 `order_room_assignment` 表经 internal bundle(hl-mp-service `MpServiceDetailAggregationService`)汇聚,对外无变化。internal 端点仅服务间可达,网关对前端直接拒绝「接口不可访问」。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无变更、无 Flyway。表 `order_room_assignment` 保留;存量数据由 internal bundle 继续读取。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 调用 GET /order/{id}/room | 返回 HTTP 200 + code 404 |
|
||||
| 调用 POST /order/{id}/room | 返回 HTTP 200 + code 404 |
|
||||
| 调用 PUT /order/{id}/room/{roomAssignmentId} | 返回 HTTP 200 + code 404 |
|
||||
| 前端经网关调用 `GET /v3/internal/order/orders/{orderId}/assignments` | 网关直接拒绝,返回「接口不可访问」(设计如此,非前端可用入口) |
|
||||
| 调用 `GET /mp/order/{orderId}/hotels` | 返回 200,roomTypeStats 从 `order_room_assignment` 经 internal bundle 读 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 房间分配 GET | 路由存在,可查询 | 路由删除,返回 HTTP 200 + code 404 |
|
||||
| 房间分配 POST | 路由存在,可新增 | 路由删除,返回 HTTP 200 + code 404 |
|
||||
| 房间分配 PUT | 路由存在,可编辑 | 路由删除,返回 HTTP 200 + code 404 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui 零调用这三个端点,无迁移负担。C 端房型统计无变化(经 internal bundle)。
|
||||
- **数据影响**:零;表与 API 读逻辑保留。
|
||||
- **其它服务**:hl-mp-service 无直接调用;fleet 无影响。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- House 域房间分配接口(§2.5 GET/POST/DELETE):无改动。
|
||||
- Internal bundle (`GET /v3/internal/order/orders/{orderId}/assignments`):保留,继续供 hl-mp-service 读。
|
||||
- `order_room_assignment` 表:保留;级联软删仍在各写口生效。
|
||||
- `RoomAssignmentVO` 与 `toRoomVO`:保留(bundle 在用)。
|
||||
- 网关配置:无改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | `GET /v3/admin/order/{id}/room` | HTTP 200 + code 404「接口不存在: …」 |
|
||||
| 2 | `POST /v3/admin/order/{id}/room` | HTTP 200 + code 404「接口不存在: …」 |
|
||||
| 3 | `PUT /v3/admin/order/{id}/room/{roomAssignmentId}` | HTTP 200 + code 404「接口不存在: …」 |
|
||||
| 4 | 阳性对照:同 Controller `GET /v3/admin/order/{id}` | 200(证明是路由被删,不是订单不存在) |
|
||||
| 5 | 服务内部端口直连读 `order_room_assignment`(internal bundle) | 读数与只读 SQL 一致(经网关 internal 不可达,这是设计) |
|
||||
|
||||
调用来源:测试网关 nginx 日志窗口内旧 `room` 三路径共 71 次,全部是脚本(Python-urllib / curl / 空 UA),零浏览器 UA。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8387
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8398
|
||||
- 替代接口:写房间分配走 API-SPEC-HOUSE §2.5 的配房接口(`POST /v3/admin/order/hotel-requirements/{requirementId}/assignments` 等);读订单房间用 `GET /v3/admin/order/orders/{orderId}/rooms`
|
||||
- API-SPEC:§8「房间分配」与 §11 错误码表已标下线
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,375 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8388"
|
||||
title: "回执接口守卫:上传加认领人校验(808110/808186,超管不豁免)、列表加房务读门(808090)、缺分片改 808184"
|
||||
consumer: "admin"
|
||||
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: "房务回执上传新增认领人校验(超管不豁免)与可选分片处理;列表新增房务读权限门(不受灰度开关控制,常开);错误码 808184 消息补五种原因的具体文案。测试环境网关已实测各分支,含缺分片 808184 新文案。前端实证(mmg 2026-09-27):uploadReceipt 走 http.upload 同一拦截器统一弹 message,808184 原因入 message 无需按 code 分支;回执入口仅房务角色页,零改动 not_required。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:房务最终确认回执接口守卫加固(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8394(原始改动) + #8399(808184 补 {0} 占位追加)
|
||||
**Issue**: #8388
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **上传回执** `POST /admin/house/assignments/requirements/{requirementId}/receipts`:
|
||||
- 新增认领人校验:需求必须被当前操作人认领,否则返回 **808110**(非本人);**SUPER_ADMIN 在本端点不豁免**,与 #8386 驳回需求口径相反。
|
||||
- 需求处于 `PENDING_CLAIM`(未认领)时先被状态闸口拦下,返回 **808186**;错误码 808116 仅作理论防御(正常调用路径不可达)。
|
||||
- 缺 file 分片 / 0 字节 file 分片改返 **808184**「回执上传失败:上传文件为空或缺少 file 分片」(改前返 HTTP 500),共 5 种原因文案。
|
||||
- 顺序:角色门(808090/808091)→ 需求存在(808100)→ 状态闸口(808186)→ 认领人校验(808110)→ 文件校验(808184)。
|
||||
|
||||
2. **回执列表** `GET /admin/house/assignments/requirements/{requirementId}/receipts`:
|
||||
- 新增房务读权限门:非房务角色(定制师、车控等)返回 **808090**。房务全角色(含组长)可见;超管可见。
|
||||
- 无认领限制(组长可看全部、他人接手前查历史回执)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
回执两个端点(上传、列表)无任何权限校验,导致:
|
||||
1. 非认领人房务可为他人上传回执,破坏追责链。
|
||||
2. 定制师、车控等非房务角色可读回执列表含 OSS 直链与酒店联系信息。
|
||||
|
||||
同一配房模块的 10 个写操作都带认领人校验;回执缺失属遗漏。缺 file 分片时返回 500 属参数处理缺陷(hl-common-log 共性坑,本单仅止血)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 上传最终确认回执 | POST | `/admin/house/assignments/requirements/{requirementId}/receipts` | 修改接口 | 加认领人校验、缺分片改 808184 |
|
||||
| 2 | 回执列表 | GET | `/admin/house/assignments/requirements/{requirementId}/receipts` | 修改接口 | 加房务读权限门(808090) |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 上传最终确认回执 `POST /admin/house/assignments/requirements/{requirementId}/receipts`
|
||||
|
||||
**VO**: `multipart/form-data: file → Result<HouseFinalizeReceiptRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务在配房面板「最终确认」区上传回执照片/PDF,证明已与酒店确认房间。仅**该需求的认领人本人**可上传;非房务角色、房务组长、非认领人(含 SUPER_ADMIN)均返回权限错误——超管在本端点**不豁免**认领归属校验,这一点与 #8386(驳回需求超管豁免)口径不同。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| requirementId | Path | Long | 是 | — | 酒店需求 ID |
|
||||
| file | Form | MultipartFile | 是(业务校验) | ≤50MB,类型自动识别 | 回执文件;缺失返 808184 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| receiptId | String | 回执 ID(雪花 ID,序列化为字符串) |
|
||||
| requirementId | String | 需求 ID(雪花 ID,序列化为字符串) |
|
||||
| orderId | String | 订单 ID(雪花 ID,序列化为字符串) |
|
||||
| fileName | String | 原始文件名 |
|
||||
| ossUrl | String | OSS 公网 URL,`domain + "/" + ossKey` 拼接,不签名 |
|
||||
| ossKey | String | OSS Object Key |
|
||||
| fileSize | Long | 文件大小(字节) |
|
||||
| fileType | String | 附件类型:`PDF` / `IMAGE` / `OTHER`(按扩展名识别) |
|
||||
| createTime | LocalDateTime | 上传时间,序列化格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /admin/house/assignments/requirements/2103126783048245250/receipts HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <room_manager token>
|
||||
Content-Type: multipart/form-data; boundary=----Boundary
|
||||
|
||||
------Boundary
|
||||
Content-Disposition: form-data; name="file"; filename="receipt.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
[binary jpeg data]
|
||||
------Boundary--
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"receiptId": "2103801203844689921",
|
||||
"requirementId": "2103126783048245250",
|
||||
"orderId": "2100542287908892650",
|
||||
"fileName": "receipt.jpg",
|
||||
"ossUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/house/finalize-receipt/...",
|
||||
"ossKey": "house/finalize-receipt/2026-09/2103126783048245250_receipt.jpg",
|
||||
"fileSize": 512000,
|
||||
"fileType": "IMAGE",
|
||||
"createTime": "2026-09-26 14:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败(非认领人)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808110,
|
||||
"message": "需求不属于当前用户",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**失败(缺分片,实测)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808184,
|
||||
"message": "回执上传失败:上传文件为空或缺少 file 分片",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 文件为空或缺 `file` 分片:`code=808184`,message「回执上传失败:上传文件为空或缺少 file 分片」(改前返 HTTP 500)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808110,
|
||||
"message": "需求不属于当前用户",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**其他错误码**:
|
||||
- **808090**:未登录或非房务角色,无权操作。
|
||||
- **808091**:房务组长为只读监督角色,无权执行该操作。
|
||||
- **808100**:需求不存在。
|
||||
- **808186**:当前状态不允许上传回执(仅 CLAIMING/PENDING_FINALIZE 可传;PENDING_CLAIM 等其他状态走这个码,不是 808116)。
|
||||
- **808116**:订单未抢单, 请先抢单再配房(状态闸口之后理论不可达,作纵深防护保留)。
|
||||
- **808184**:回执上传失败:{0},`{0}` 按触发原因取以下固定文案之一——「上传文件为空或缺少 file 分片」/「文件大小超过 50MB」/「OSS 未配置」/「读取上传文件失败」/「文件存储服务暂不可用,请稍后重试」;异常原文只进服务端日志,不回显给客户端。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **认领校验**:需求 `claimer_id` 必须等于当前操作人;**超管不豁免**,对他人认领的需求上传同样返回 808110。与 #8386(驳回需求,超管豁免)口径不同,前端不要套同一套按钮逻辑。
|
||||
- **校验顺序**:写门(808090/808091)→ 需求存在(808100)→ 状态闸口(808186)→ 认领归属(808116/808110)→ 文件校验(808184)。
|
||||
- **文件分片**:`file` 分片改为可选(`required=false`),缺失走 Service 内已有的空文件分支报业务码 808184(改前是 HTML form 漏填触发 `MissingServletRequestPartException`,落到兜底 `Exception` 处理器返 HTTP 200+code 500「系统繁忙」并打 ERROR 日志)。
|
||||
- **状态限制**:仅 CLAIMING(配房中)与 PENDING_FINALIZE(待确认)两个阶段可上传;PENDING_CLAIM(未抢单)等其他状态返 808186,不是 808116。
|
||||
- **无事务**:`upload()` 不加 `@Transactional`;OSS `putObject` 成功后才 `insert`,`insert` 失败可能残留孤儿 OSS 对象(由清理 job 兜底);OSS 失败(`putObject` 抛异常)返 808184 且不落库。
|
||||
|
||||
---
|
||||
|
||||
### 2. 回执列表 `GET /admin/house/assignments/requirements/{requirementId}/receipts`
|
||||
|
||||
**VO**: `→ Result<List<HouseFinalizeReceiptRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务或超管查看某需求的全部回执列表(谁上传的、何时、文件链接)。非房务角色返回 808090;房务全角色可见无限制。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| requirementId | Path | Long | 是 | — | 酒店需求 ID |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| receiptId | String | 回执 ID(雪花 ID,序列化为字符串) |
|
||||
| requirementId | String | 需求 ID(雪花 ID,序列化为字符串) |
|
||||
| orderId | String | 订单 ID(雪花 ID,序列化为字符串) |
|
||||
| fileName | String | 原始文件名 |
|
||||
| ossUrl | String | OSS 公网 URL,不签名 |
|
||||
| ossKey | String | OSS Object Key |
|
||||
| fileSize | Long | 文件大小(字节) |
|
||||
| fileType | String | 附件类型:`PDF` / `IMAGE` / `OTHER` |
|
||||
| createTime | LocalDateTime | 上传时间,序列化格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/house/assignments/requirements/2103126783048245250/receipts HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <room_manager token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功(房务)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"receiptId": "2103801203844689921",
|
||||
"requirementId": "2103126783048245250",
|
||||
"orderId": "2100542287908892650",
|
||||
"fileName": "receipt_1.jpg",
|
||||
"ossUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/house/finalize-receipt/...",
|
||||
"ossKey": "house/finalize-receipt/2026-09/2103126783048245250_receipt_1.jpg",
|
||||
"fileSize": 512000,
|
||||
"fileType": "IMAGE",
|
||||
"createTime": "2026-09-26 14:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**失败(定制师)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808090,
|
||||
"message": "未登录或非房务角色,无权操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 无回执:`code=200`,`data=[]`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808090,
|
||||
"message": "未登录或非房务角色,无权操作",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- **808090**(新增):非房务角色返回;该读门调 `HouseReadGuard.assertHouseReadPermission()`,**不受** #8390 引入的灰度开关 `group-batch.acl.enforce.house-read-role` 控制,是常开的角色门。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **角色放行**:ROOM_MANAGER / house_keeper_lead / SUPER_ADMIN 均可读;零角色(#7609 G-2 保留项)亦放行。
|
||||
- **无认领限制**:组长可看全部需求回执(监督权),他人接手后也可查历史。
|
||||
- **直链有效期**:测试桶直链公共可读(无签名);生产桶读策略未定,前端不得缓存直链。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 调用方法 | 说明 |
|
||||
|------|---------|------|
|
||||
| ✅ 认领人上传 | `POST /admin/house/assignments/requirements/{id}/receipts`,`multipart: file=@...` | 该需求的当前认领人上传,返回 200 |
|
||||
| ✅ 房务查列表 | `GET /admin/house/assignments/requirements/{id}/receipts` | 返回 200 + 全部回执 |
|
||||
| ✅ 组长查列表 | 同上(house_keeper_lead 身份)| 无限制,200 返回 |
|
||||
| ❌ 非认领人上传(含 SUPER_ADMIN) | — | 返回 808110,不落库;超管在本端点**不豁免**认领归属校验 |
|
||||
| ❌ 未认领需求(PENDING_CLAIM)上传 | — | 返回 808186(先过状态闸口) |
|
||||
| ❌ 定制师查列表 | — | 返回 808090 |
|
||||
| ❌ 缺 file 分片 | 空 multipart 或无 file 字段 | 返回 808184 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无变更、无 Flyway。`house_finalize_receipt` 表与逻辑保留;认领人校验在内存判断,无新增存储。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 认领人上传 | 返回 200,回执入库入 OSS |
|
||||
| 非认领人上传(含 SUPER_ADMIN) | 返回 808110,无新行 |
|
||||
| 转单后原人上传 | 返回 808110(新人成认领人,原人失去权限) |
|
||||
| PENDING_CLAIM 需求上传 | 返回 808186(状态闸口先于认领校验) |
|
||||
| 组长列表查询 | 返回 200,全列表 |
|
||||
| 定制师列表查询 | 返回 808090,无数据 |
|
||||
| 缺 file 分片 / 0 字节 file 分片 | 返回 808184(HTTP 200、code 业务码,5 种原因文案) |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 非认领人上传(含超管) | 返回 200,入库 | 返回 808110 |
|
||||
| 缺 file 分片 | HTTP 500、ERROR 日志 | HTTP 200、code 808184,仅 WARN 日志 |
|
||||
| 定制师查列表 | 返回 200 + 列表 | 返回 808090 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui 回执上传与列表操作收新错误码(808090/808091/808110/808186/808184);需处理新文案提示。
|
||||
- **前端要动的**:
|
||||
- 上传失败按错误码区分:「权限不足」(808090/808091)、「非本人(含超管)」(808110)、「需求未认领」(808186)、「文件」(808184,5 种原因文案,需在 `message` 里判断具体原因而非只判 code)。
|
||||
- 定制师等非房务角色不再能查回执列表,改改导航或隐藏入口。
|
||||
- **数据影响**:无;仅加校验,不改历史回执。
|
||||
- **其它服务**:hl-mp-service、fleet 无直接调用。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 房务订单详情页回执展示逻辑(虽然定制师不再能查全列表,已登录进详情页的现有回执链接仍有效)。
|
||||
- 回执删除接口(无,但实体带 `@TableLogic` 支持软删)。
|
||||
- 其他配房写操作(转单、释放、分房等),无改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | 认领人上传 | code 200,新行 uploader_id = 认领人;已软删清理 |
|
||||
| 2 | 非认领人上传 | code 808110,无新行 |
|
||||
| 3 | PENDING_CLAIM 需求上传 | code 808186 |
|
||||
| 4 | SUPER_ADMIN 对他人认领的需求上传 | code 808110(不豁免) |
|
||||
| 5 | 缺 file 分片(multipart 只带无关字段)×2 | HTTP 200 + `{"code":808184,"message":"回执上传失败:上传文件为空或缺少 file 分片"}` |
|
||||
| 6 | 0 字节 file 分片 | 同 #5,同码同文案 |
|
||||
| 7 | 房务(认领人本人)查列表 | code 200,全部回执 |
|
||||
| 8 | 定制师 / VEHICLE_MANAGER 查列表 | code 808090,无数据 |
|
||||
| 9 | house_keeper_lead / 非认领 ROOM_MANAGER / SUPER_ADMIN 查列表 | code 200,全部回执 |
|
||||
|
||||
#5、#6 场景 order-v3 日志对应 3 条 WARN `Business error [order.808184]`,无 `Unexpected error` ERROR 行。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8388
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8394(原始改动)、https://git.1814.love/wx/HL/pulls/8399(808184 补 `{0}` 占位追加)
|
||||
- 相关单号:#8386(认领人校验,超管在该单**有**豁免,口径与本单相反)、#8390(读权限门同配方)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8389"
|
||||
title: "房务配房:下线 POST /v3/admin/order/assignments/{id}/rooms 家庭维度写接口"
|
||||
consumer: "admin"
|
||||
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: "house 域家庭维度房间分配写口(POST)已下线,GET 端点由 #8390 补读门。hl-ui 零调用。前端实证(mmg 2026-09-27):POST assignments/{id}/rooms 零调用,not_required 复核确认。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3:下线家庭维度房间分配写接口(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #8396
|
||||
**Issue**: #8389
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
`POST /v3/admin/order/assignments/{assignmentId}/rooms`(家庭维度房间分配)已删除,连同 `RoomAssignReqVO`/`RoomAssignRespVO` 与错误码 **808132**(入住人数必须 > 0)一并删除,码号不复用。
|
||||
|
||||
调用返回 HTTP 200 + `code: 404`(不是 HTTP 404)。GET 端点 `/v3/admin/order/orders/{orderId}/rooms` 由 #8390 补读权限门(ROOM_MANAGER/house_keeper_lead/SUPER_ADMIN 可读;非房务返 808090)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
该端口对 `rooms[].roomGroupNo` 与 `rooms[].travelerCount` 无出行人校验,会写入脏数据(`F99` 分组、`travelerCount=99` 等)。无调用方,计划下线避免继续钉住问题。
|
||||
|
||||
表 `house_room_assignment` 与 GET 端点保留(级联软删仍有效);仅删写接口,该表此后无写入方,GET 端点出参里对新数据 `roomAssignments` 恒为空数组。`house_room_assignment` 只被 `HouseAssignmentService` 读;C 端聚合读的是 #8387 相关的 `order_room_assignment` 表(不同表),两者不要混淆。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 房间分配写 | POST | `/v3/admin/order/assignments/{assignmentId}/rooms` | 删除 | 路由已删,返回 404 |
|
||||
|
||||
GET 端点 `/v3/admin/order/orders/{orderId}/rooms` 由 #8390 补读门(不在本单)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 房间分配写 `POST /v3/admin/order/assignments/{assignmentId}/rooms`
|
||||
|
||||
**VO**: `已删除`
|
||||
|
||||
**状态**:已删除。路由不存在,返回 404。
|
||||
|
||||
#### 使用场景
|
||||
|
||||
该接口已删除。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 已删除 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| — | — | 已删除 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/assignments/2103126783048245250/rooms HTTP/1.1
|
||||
Host: api.test.1814.love:9443
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
HTTP 状态码 **200**(非 404),业务错误码 404:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/assignments/2103126783048245250/rooms",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用。前端应只判 `code`,不依赖 HTTP 状态码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/assignments/2103126783048245250/rooms",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 接口已删除,路由不存在;调用返回 HTTP 200 + `code: 404`。
|
||||
- 错误码 808132(入住人数必须 > 0)随本单一并删除,码号不复用。
|
||||
- hl-ui 零调用;测试网关 nginx 日志窗口内该路径 53 次调用全部是脚本(python-requests / Python-urllib),零浏览器 UA。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
该接口已删除,不可调用。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无变更、无 Flyway。表 `house_room_assignment` 保留;级联软删仍有效。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 调用 POST /assignments/{id}/rooms | 返回 HTTP 200 + code 404 |
|
||||
| 调用 GET /orders/{id}/rooms | 仍在(200);#8390 补读门后,非房务返 808090 |
|
||||
| `house_room_assignment` 表 | 保留,此后无写入方,级联软删仍对存量数据有效 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| POST 家庭房间 | 路由存在,可写 | 路由删除,返回 404 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:hl-ui 零调用,无前端迁移。
|
||||
- **数据影响**:零;表与 GET 保留。
|
||||
- **其它服务**:无。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 房间分配 GET:保留(#8390 补读门)。
|
||||
- 其他配房写(delete/updatePlacement 等):无改动。
|
||||
- 级联软删:保留。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:测试环境 order-v3 `1f65d7894`(含 e2313790b)。网关 `api.test.1814.love:9443`。
|
||||
|
||||
| # | 场景 | 实测结果 |
|
||||
|---|---|---|
|
||||
| 1 | `POST /v3/admin/order/assignments/{assignmentId}/rooms` | HTTP 200 + code 404「接口不存在: …」 |
|
||||
| 2 | `GET /v3/admin/order/orders/{orderId}/rooms` | 200(仍在) |
|
||||
|
||||
调用来源:测试网关 nginx 日志窗口内该路径 53 次,全部是脚本(python-requests / Python-urllib),零浏览器 UA。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love/wx/HL/issues/8389
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8396
|
||||
- 关联:#8390(补读权限门)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,117 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "答复前端《待后端支持事项汇总》(2026-09-26)非车务五项:四项后端早已交付且 hl-admin 已接入可销项,应收台账菜单缺口已记 #8369"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "答复 mmg 2026-09-26《前端待后端支持事项汇总》第 1/2/3/4/7 项(车务第 5/6/8/9 项不在本篇)。结论:①002 待收已由 #8045 unpaidAmount 交付(不是 balanceAmount),BatchHero.vue:147 已直显;②0 子订单行 chips 自 #7189 起六项 TODO,TEST 294 行 chips=null 为 0;③003 totalPrice + birthdayInTrip 跨年修正已交付(#6905 §十一 / #7066),estimatedCost 与预计毛利列已随 #7536 下线;④008 opsStage 已交付(#6926,#8271 起七桶),hl-admin index.vue:547 已传参;⑦应收台账 sys_menu 确实缺(TEST 库收款管理下只有 0601~0603),已记 #8369,前端零改动。前 4 项接口零变更、前端零代码改动,只需从待办清单销项。2026-09-26 经 TEST 网关实测。 前端 2026-09-26 已处理:第 1/2/3/4 项逐一 grep 实证(BatchHero.vue:147 unpaidAmount 直显 / PeriodRow.vue:326 chips 兜底 / batch index.vue 导出已传 opsStage / estimatedCost 全仓零消费仅注释),确认已接入并销项;sync-log 挂起区四条已关闭、#8248 菜单项关联 #8369;给后端的清单文档已同步修订。零代码改动,判 not_required(hl-admin 136973bf 为 sync-log 销项提交)。"
|
||||
updated_at: "2026-09-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 答复:前端《待后端支持事项汇总》(2026-09-26)非车务五项
|
||||
|
||||
> **端**: 管理后台 | **来源**: mmg 2026-09-26《前端待后端支持事项汇总》 | **范围**: 第 1 / 2 / 3 / 4 / 7 项;车务第 5 / 6 / 8 / 9 项不在本篇
|
||||
>
|
||||
> **本篇不含任何接口变更**:前 4 项后端早已交付,hl-admin 也早已接入;第 7 项的缺口已记工单。前端要做的只有一件事——**更新待办清单**。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论先行
|
||||
|
||||
| # | 清单事项 | 实际状态 | 前端动作 |
|
||||
|---|---|---|---|
|
||||
| 1 | 002 补「待收尾款」字段 | **已交付**:#8045 新增 `unpaidAmount`(字段名不是 `balanceAmount`) | 销项。`BatchHero.vue:147` 已直显,`FinanceTab.vue` 已展示 `totals/items.unpaidAmount` |
|
||||
| 2 | #6905 行接口 `chips` 恒 null | **已交付**:#7189 起 0 子订单行六项固定 `TODO`,不再为 null | 销项。`PeriodRow.vue:326` 的 `chips \|\| {}` 兜底留着无害 |
|
||||
| 3 | #6929 003 补 `totalPrice` + 生日跨年 | **已交付**:#6905 §十一 / #7066(#7113 收敛字段名) | 销项。「`totalPrice − estimatedCost` 算预计毛利」一条**作废**:`estimatedCost` 与预计毛利列已随 #7536 下线 |
|
||||
| 4 | #6926 008 导出补 `opsStage` | **已交付**:#6926,#8271 起按七桶过滤 | 销项。`index.vue:547` 已传 `opsStage` |
|
||||
| 7 | 应收台账 sys_menu 补配 | **确实缺**:已记 **#8369**(hl-user-service Flyway 补菜单种子) | 前端零改动;清单保留此项,关联 #8369 |
|
||||
|
||||
> 行号均指 hl-admin `origin/v2.1` @ `b53414d4`。
|
||||
|
||||
---
|
||||
|
||||
## 二、逐项证据(2026-09-26 TEST 网关实测 + `origin/dev-v3` @ `355c578d1` 源码)
|
||||
|
||||
### 1. 002 团期详情:待收 = `unpaidAmount`
|
||||
|
||||
- **字段**:`unpaidAmount` = `max(0, receivableAmount − receivedAmount)`,恒非负、恒非 null,字符串金额(`GroupBatchDetailRespVO.unpaidAmount`,#8045)。
|
||||
- **TEST 实测**:列表前 40 个团逐个调 `GET /v3/admin/order/group-batch/{groupBatchId}`,`unpaidAmount` 缺失或不等于公式的 **0 个**。样例团 `2103733823122690049`:
|
||||
|
||||
```json
|
||||
{ "receivableAmount": "4000.00", "receivedAmount": "2000.00", "unpaidAmount": "2000.00",
|
||||
"totalReceivable": "4000.00", "totalReceived": "2000.00" }
|
||||
```
|
||||
|
||||
- **清单原文的两处出入**:
|
||||
- 「002 只下发 `totalReceivable` / `totalReceived`」——这两个是 #7535 标了废弃的旧名,002 同时下发新名 `receivableAmount` / `receivedAmount` 和 `unpaidAmount`;
|
||||
- 字段名请用 `unpaidAmount`,**不要叫 `balanceAmount`**:同页子订单项已有 `balanceAmount`,是逐户「应收 − 已退 − 已付」、取消单归 0 的另一套口径,混名会在同一屏出现两套数。
|
||||
- **口径提醒(沿用 #8045)**:`unpaidAmount` 按毛累计已付算,退款不回减;逐户扣退款的权威待收在财务 tab 的 `items[].unpaidAmount` / `totals.unpaidAmount`(财务 tab 顶层 `unpaidAmount` 与 002 同源同值)。hl-admin 现有实现与此一致。
|
||||
- **既有交接件**:`2026-09/21_8045_团期详情返回整团待收unpaidAmount-修改接口-管理后台.md`(verified 2026-09-21,hl-admin `e680cf10`)。
|
||||
|
||||
### 2. 001 团期看板行:`chips` 不再为 null
|
||||
|
||||
- **源码**:`GroupBatchQueryService.resolveChipsMap` 对本页每个 `productBatchId` 都派生六芯片,0 子订单按零户口径出六项 `TODO`(#7189,两条基底统一);`order_group_batch.product_batch_id` 为 NOT NULL,不存在取不到芯片的行。
|
||||
- **TEST 实测**:
|
||||
- `GET /v3/admin/order/group-batch?scope=ALL` 全量 **294 行**,`chips=null` **0 行**;其中 `orderCount=0` 的 76 行六项均为 `TODO`,样例团 `2103060033518661633`:
|
||||
|
||||
```json
|
||||
{ "hotel": "TODO", "vehicle": "TODO", "guide": "TODO", "photo": "TODO", "contract": "TODO", "insurance": "TODO" }
|
||||
```
|
||||
|
||||
- 带 `productId` 的合并基底(产品 `2100839045562077186`,11 行,其中 7 行未建团):`chips=null` **0 行**。
|
||||
- **既有交接件**:`2026-09/07_7189_团期看板产品全班期基底与scope范围筛选-修改接口-管理后台.md`(verified 2026-09-08)。
|
||||
|
||||
### 3. 003 团期子订单列表:`totalPrice` 与 `birthdayInTrip`
|
||||
|
||||
- **`totalPrice`**:本户应收 = `orderAmount + surchargeAmount − discountAmount`(下限 0),JSON 字符串两位小数;取消单返回 `"0.00"`(清单写的 `"0"` 以 `"0.00"` 为准)。
|
||||
- **`birthdayInTrip` 跨年修正**:行程跨年时按出团年、返团年各年化比较一次,任一落在 `[departDate, endDate]` 即 true(`GroupBatchQueryService.inTripWindow`,#6929)。
|
||||
- **TEST 实测**:遍历有子订单的团共 **510 条**子订单,`totalPrice` 全部存在且均为字符串;`includeTravelers=true` 时 `travelers[]` 字段为 `name / type / age / birthdayInTrip`。本列表不返回已取消子订单,本次未取到取消单样例,取消单归 `"0.00"` 以 #7066 于 2026-09-04 的 TEST 8 组实测为准。
|
||||
- **预计毛利作废**:清单「解除后可用 `totalPrice − estimatedCost` 算预计毛利(`estimatedCost` 已可用)」已过时——#7536 已从 003 删除 `estimatedCost`,预计毛利列已下线、毛利改在核单页看(hl-admin `SubOrderTable.vue:114`、`RosterTable.vue:25` 的注释已写明)。
|
||||
- **既有交接件**:`2026-09/01_6905_团期看板-4-接口对齐补全-GB-ADM-000-001-002-003-修改接口-管理后台.md` §十一、`2026-09/04_7066_团期子订单列表补应收总额-修改接口-管理后台.md`(均 verified)。
|
||||
|
||||
### 4. 008 导出:`opsStage` 过滤生效
|
||||
|
||||
- **源码**:`GroupBatchBoardStatsController.export` 接收 `opsStage`,Service 经 `GroupBatchStageBuckets.statusCodesOf(opsStage)` 过滤;#8271 起按七桶取值,旧八桶 code 仍兼容。
|
||||
- **TEST 实测**(`GET /v3/admin/order/group-batch/export`,`scope=ALL`):
|
||||
|
||||
| 请求 | CSV 数据行数 | 同条件列表中该桶行数 |
|
||||
|---|---|---|
|
||||
| 不带 `opsStage` | 294 | 294 |
|
||||
| `opsStage=CONFIGURE` | 205 | 205 |
|
||||
| `opsStage=RECRUIT` | 36 | 36 |
|
||||
|
||||
- hl-admin `src/views/order-v2/batch/index.vue:547` 已传 `opsStage: f.opsStage || undefined`。
|
||||
- **既有交接件**:`2026-09/04_6926_导出补-opsStage-筛选-+-统计条自校验死代码修正(修改接口)-修改接口-管理后台.md`(verified 2026-09-04)、`2026-09/24_8271_团期看板与详情按六节点展示看板改七桶-修改接口-管理后台.md`。
|
||||
|
||||
### 7. 应收台账菜单:确实缺,已记 #8369
|
||||
|
||||
- **TEST 库实查**:`hl_user_service.sys_menu` 中「收款管理」(`menu_id=2026090900000000600`)下只有 `…0601` 业务外收入、`…0602` 公司借入、`…0603` 公司借款收回三项,没有应收台账;`origin/dev-v3` 的 Flyway 里也没有这条菜单。
|
||||
- **工单**:[#8369](https://git.1814.love/wx/HL/issues/8369) 补挂三级菜单「应收台账」,`path` 与 `component` 均为 `/finance/receipt/receivable`(**带前导 `/`**,与同级「业务外收入」等一致;清单原文写的 `finance/receipt/receivable` 以带 `/` 为准),授权 `SUPER_ADMIN / ADMIN / FINANCE`。
|
||||
- **接口**:`GET /admin/finance/receipt/receivable/page`(#8191)已在 TEST 可用,本项与接口无关。
|
||||
- **前端**:零改动。页面 `src/views/finance/receipt/receivable/index.vue` 已就绪,菜单种子落库后侧栏自然出现。
|
||||
|
||||
---
|
||||
|
||||
## 三、给前端待办清单的建议
|
||||
|
||||
清单第 1~4 项沿用的是 #6905 / #6926 / #6929 复审返工期的描述,这三张单 2026-09-01~09-04 已关闭,对应交接件 hl-admin 也都已 verified。生成下一版清单前,建议先按 changelog 仓对应文件 frontmatter 的 `frontend_status` 过一遍,已 verified 的直接销项。
|
||||
|
||||
## 四、影响范围
|
||||
|
||||
- **接口**:零变更(本篇只答复,不改任何契约)。
|
||||
- **前端代码**:零改动要求。
|
||||
- **前端动作**:清单第 1 / 2 / 3 / 4 项销项;第 7 项关联 #8369。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 答复人:jw(后端)
|
||||
- 相关工单:#8045、#7189、#6905、#6929、#7066、#7536、#6926、#8271、#8191、#8369
|
||||
@@ -0,0 +1,334 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7608"
|
||||
title: "团期转订单收口判权:transfer-in 接 group-batch:manage、transfer-candidates 接 group-batch:view(589507),各带 nacos 灰度开关"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "已部署 dev-v3 到 TEST 并实打验证:五角色矩阵逐格吻合、nacos 两开关三态往返(含还原后复现)、观察日志反证强制态未走旁路。【2026-09-27 mmg】前端判 not_required(零代码,用户拍板守既定架构):getTransferCandidates/transferInSubOrder 均无 silentError,589507 由 request.js 拦截器透 message;转订单入口=canTransferOrder(batchStatus 白名单)&&!isGroupBatchManager,不接 hasPermission;§四/§六.7 建议按码控制可见性与既定架构「可见即可点+589507 兜底、后端不下发 group-batch 按钮码无码可接」冲突,维持现状。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期转订单两个端点收口判权(批 2)
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: `#8416`(已合入 `dev-v3`,合并后 head `22f9fb83d`)
|
||||
**Issue**: #7608
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **`transfer-in`(转订单写口)从「任何后台角色都能调」变为「必须持 `group-batch:manage`」,无权限返回 `589507`。** 改前**零判权**,而它**涉钱**:改应收金额、改归属两列、改客户出行日期、动两期名额,且**没有反向端点**。
|
||||
|
||||
🔴 **`transfer-candidates`(转单弹窗的搜索下拉)从「连权限码都不判」变为「必须持 `group-batch:view`」。** 改前它跨团期返回订单号 / 客户名 / 期号,并支持手机号整串精确检索,而且是上面那个写口的**攻击目标供给器**。
|
||||
|
||||
🟡 **两个端点必须成对看**:只收口写口会让弹窗「能搜不能转」,只收口读口会「能转搜不到」。本单同批上线。
|
||||
|
||||
🟢 **响应结构、成功码、业务错误码全部不变。** 有权限的调用方行为与改前逐字节一致。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`#7455` 自称做了「全端点扫描」,但漏掉 `GroupBatchActionController` 里的 4 个端点。根因是审计方法按**类**计数 `GroupBatchPermissionGuard` 命中数——该类因 `advance` / `recheck-material-gate` 两处有 guard 就被整类归入「已判权」。
|
||||
|
||||
收口分四批:批 0 给裸端点加旁路观察探针;批 1 收口 `cancel-group`(已上线);**本单是批 2**;批 3(`withdraw` 提交)待定案。
|
||||
|
||||
**为什么批 2 要等**:码的选择依赖批 0 的观察窗。观察窗 2026-09-13~09-27 满 14 天,结论是这两个端点在留存日志里**零调用、无意外调用方**,故按静态推导定案。
|
||||
⚠️ 这是「**没有反证**」而非「**有正证**」:TEST 上 order-v3 跑两个实例,8086 实例 09-13~09-25 的日志已不可取(落点随 09-23 端口迁移挪过)。对冲手段就是下面两个 nacos 开关。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 转订单(子订单跨期转入) | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` | 修改 | **新增判权**:需 `group-batch:manage`,无权限 `589507`;响应结构与业务错误码不变 |
|
||||
| 2 | 转订单候选子订单列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/transfer-candidates` | 修改 | **新增判权**:需 `group-batch:view`,无权限 `589507`;响应结构不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 转订单 `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in`
|
||||
|
||||
**VO**: `TransferSubOrderReqVO` → `Result<TransferSubOrderRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台团期详情「转订单」弹窗:把另一期的子订单转入本期。**本单起需要 `group-batch:manage` 权限。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | **行为不变** |
|
||||
| orderId | Path | Long | ✅ | 待转入的子订单 ID | **行为不变** |
|
||||
| fromGroupBatchId | Body | Long | ✅ | 转出期团期主键 | **行为不变** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `TransferSubOrderRespVO` | **结构完全不变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2098606570236481538/sub-order/2103877643344691202/transfer-in HTTP/1.1
|
||||
X-Admin-Id: 1001
|
||||
X-Admin-Role: ADMIN
|
||||
Content-Type: application/json
|
||||
|
||||
{"fromGroupBatchId": 2098979230774657025}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无分页语义。**降级**:nacos `group-batch.acl.enforce.transfer-in` 置 `false` 时,判权**退化为旁路观察日志**(`GB_ACL_PROBE`)而非静默放行——所有角色恢复放行,行为与改前一致,但每次调用仍在服务端留痕。开关默认 `true`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方不持 `group-batch:manage` | 🆕 **本端点新增**(码本身早已存在) |
|
||||
| 589500 | — | 团期不存在 | 不变 |
|
||||
| 其它业务码 | — | 转入的各项业务前置 | 不变 |
|
||||
|
||||
`589507` 实打响应体(TEST,2026-09-27):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **拒绝时零写入**:判权在 Controller 入口,排在 Service 任何库读写之前。
|
||||
- **`OrderViewGuard.assertNotGroupBatchManagerWrite()` 保留**:#8154 起团期管理员(GBM)「只能看不能改」,那道拦在判权之前、只挡这一个角色,两者并存不互相取代。
|
||||
- **取 `manage` 不取 `view`**:本端点涉钱且无反向端点,取写向最高码。
|
||||
|
||||
---
|
||||
|
||||
### 2. 转订单候选列表 `GET /v3/admin/order/group-batch/:groupBatchId/transfer-candidates`
|
||||
|
||||
**VO**: `Result<List<TransferCandidateVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
转订单弹窗的搜索下拉:候选 = 同产品其他可转期下的可转子订单。keyword 匹配订单号 / 客户名 / 期号(模糊)与手机号(**整串精确**,加密列不支持前缀模糊)。**本单起需要 `group-batch:view` 权限。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | **行为不变** |
|
||||
| keyword | Query | String | ❌ | 订单号 / 客户名 / 期号 / 手机号(整串) | **行为不变** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `List<TransferCandidateVO>` | **结构完全不变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2098606570236481538/transfer-candidates?keyword=GT-26-0085 HTTP/1.1
|
||||
X-Admin-Id: 1001
|
||||
X-Admin-Role: ADMIN
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无候选时 `data: []`(**行为不变**)。**降级**:nacos `group-batch.acl.enforce.transfer-candidates` 置 `false` 时退回批 0 形态——只记角色、不发 Feign 判权。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方不持 `group-batch:view` | 🆕 **本端点新增** |
|
||||
|
||||
`589507` 实打响应体(TEST,2026-09-27):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **拒绝时不查库**:判权排在查询之前,被拒的请求拿不到任何候选数据。
|
||||
- ⚠️ **本单推翻了批 0 自己写的「高频下拉不判权」**:那条理由(逐字触发、每次多一跳 Feign 到 user-service)成立,但挡不住泄露面。真实测出下拉不可接受时,出路是给守卫加短 TTL 本地缓存(**另立单**),不是把开关关掉长期不判。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 前端应按权限码控制「转订单」入口:写口按 `group-batch:manage`,搜索下拉按 `group-batch:view`。
|
||||
- `589507` 建议直接透出 `message`,无需前端自造提示。
|
||||
- 权限码与角色绑定**没有运行期写入口**(只能走 Flyway 种子)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
**无 DDL、无数据迁移、无 Flyway 脚本。**
|
||||
|
||||
两个码及其角色授予由既有种子提供:`group-batch:manage`(`V20260906_002`:ADMIN / SUPER_ADMIN)、`group-batch:view`(`V20260831_002` ADMIN/FINANCE/SUPER_ADMIN + `V20260917_001` GROUP_BATCH_MANAGER + `V20260918_005` CUSTOMIZER)。**hl-user-service 零改动** ⇒ 不涉及 #7154 那条「user-service 迁移必须先发」的铁律。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 有权限的调用方:与改前逐字节一致。
|
||||
- 无权限的调用方:改前执行成功,改后 `589507` 且零写入 / 不查库。
|
||||
- 角色头缺失:`GroupBatchPermissionGuard` **fail-closed**,`roleKey` 为空一律 `589507`(既有口径,行为不变)。
|
||||
- user-service 不可达:权限查询 `catch (RuntimeException) → false`,即 **fail-closed**。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 角色 | `transfer-in` 改前 → 改后 | `transfer-candidates` 改前 → 改后 |
|
||||
|------|---|---|
|
||||
| `SUPER_ADMIN` | 正常 → 正常 | 正常 → 正常 |
|
||||
| `ADMIN` | 正常 → 正常 | 正常 → 正常 |
|
||||
| `FINANCE` | **正常 → 589507** | 正常 → 正常(持 view) |
|
||||
| `CUSTOMIZER` | **正常 → 589507** | 正常 → 正常(`V20260918_005` 起持 view) |
|
||||
| `GROUP_BATCH_MANAGER` | 589xxx(#8154 已挡)→ 不变 | 正常 → 正常(持 view) |
|
||||
| `ROOM_MANAGER` / `VEHICLE_MANAGER` | **正常 → 589507** | **正常 → 589507** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **前端**:需按两个码分别控制写口与下拉的可见性,否则无码角色会点到必然失败的入口。响应结构无变化,无需改解析逻辑。
|
||||
- **后端**:无下游契约变化。`transfer-candidates` 每次调用多一跳 Feign(逐字下拉,可感知)。
|
||||
- **回滚**:改对应 nacos key 为 `false`,批 1 实测热刷新 ≈7s,**不需重发服务**。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- `cancel-group`(批 1 已收口)、成团 / 流团 / 名额调整 / 预支 / 确认:判权口径不变。
|
||||
- `withdraw` 提交:**本批不动**,仍只有旁路观察。它的收口是批 3,因为「提交集合是否该等于审批集合」需要定案(接 `manage` 会让两个集合相同,毁掉 #7100 建立的两人制)。
|
||||
- 小程序端(`consumer: mp`):本单端点均为管理后台端点,无影响。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-09-27 11:31~11:37
|
||||
**构建身份**:部署前 `ecc92b95c`(旧字节,批 0 形态)→ 部署后 `22f9fb83d`(本单合并提交),部署前后各打一轮,故下表是**对照**而非单点。
|
||||
|
||||
### 8.1 五角色矩阵(部署后,强制态)
|
||||
|
||||
| 角色 | `transfer-in`(接 `manage`) | `transfer-candidates`(接 `view`) |
|
||||
|---|---|---|
|
||||
| SUPER_ADMIN | `589500` 过守卫 | `589500` 过守卫 |
|
||||
| ADMIN | `589500` 过守卫 | `589500` 过守卫 |
|
||||
| CUSTOMIZER | **`589507`** 拦截 | `589500` 过守卫 |
|
||||
| FINANCE | **`589507`** 拦截 | `589500` 过守卫 |
|
||||
| ROOM_MANAGER | **`589507`** 拦截 | **`589507`** 拦截 |
|
||||
|
||||
部署前同一组请求:**十格全部非 `589507`**(旧字节只旁路观察不拦截),故新增拦截可归因到本单字节。
|
||||
|
||||
> **为什么这组读数能证明码选对了**:CUSTOMIZER 在读口过、在写口被拦。若 `transfer-candidates` 误接 `manage`,CUSTOMIZER 读口也会被拦;若误接更宽的口径,ROOM_MANAGER 读口不会被拦。这一格同时验证了**码的选择**和 `V20260918_005` 给 CUSTOMIZER 授 `view` 的**种子实际状态**。
|
||||
> **未只用超管自测**:ADMIN 过、CUSTOMIZER/FINANCE/ROOM_MANAGER 被拦,均为非超管角色,绕开了 user-service 的超管短路放行。
|
||||
|
||||
### 8.2 nacos 灰度开关三态往返
|
||||
|
||||
配置 `hl-order-service-v3-test.yml`(`tenant=test`)原本**不含** `group-batch.acl.*` 键,即跑代码默认值 `true`。
|
||||
|
||||
| 态 | 配置 | CUSTOMIZER `transfer-in` | ROOM_MANAGER `transfer-candidates` | ADMIN(对照) | `GB_ACL_PROBE` 观察日志 |
|
||||
|---|---|---|---|---|---|
|
||||
| A | 无键(默认 `true`) | `589507` | `589507` | `589500` | **无** |
|
||||
| B | 两键置 `false` | `589500` | `589500` | `589500` | **有** |
|
||||
| C | 逐字节还原 | `589507` | `589507` | `589500` | **无** |
|
||||
|
||||
- 热刷新耗时:发布后 12s 内生效(态 A→B 时刻 11:36:38→11:36:52)。
|
||||
- **还原已校验到字节**:`md5` 前后同为 `c2206934960057f70b7173159046dc54`,`cmp` 一致;还原动作挂在 `trap EXIT` 上,探针失败也不会把 TEST 留在 `false`。
|
||||
|
||||
**态 B 的放行是「退回旁路观察」而不是「判权整体失效」**——两条方向相反的证据:
|
||||
|
||||
1. 正面:态 B 期间日志记下
|
||||
`endpoint=GB-ADM-071 transfer-in roleKey=CUSTOMIZER intendedPermission=group-batch:manage hasPermission=false enforced=false`
|
||||
即「本应判 `manage`、该角色实际不持有、但当前未强制」。守卫算过了权限,只是没拦。
|
||||
2. 反面:态 A 与态 C 的时间窗内 `GB_ACL_PROBE` **零命中**。若强制分支底下还在跑观察,这里会有日志。零命中说明走的是 `require()` 而非 `observe()`,`if/else` 是真互斥。
|
||||
|
||||
> 附带加固 8.1:态 B 六条日志里 `transfer-in` 落 `http-nio-8186`、`transfer-candidates` 落 `http-nio-8086`,**两个 order-v3 实例都是新字节**,8.1 的矩阵不是「只有一个实例部署到」的假象。
|
||||
> `transfer-candidates` 的观察日志记 `intendedPermission=(not-evaluated)`,与代码里用 `observeRoleOnly()`(不查权限只记角色)一致。
|
||||
|
||||
### 8.3 本地全量对照(AC-10)
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| 范围 / 结果 | 1000 个测试类,`Tests run: 13614, Failures: 11, Errors: 10, Skipped: 7`,零 OOM,31 分钟 |
|
||||
| 定责 | 10 个失败类**全部**在基底 `57199b539` 上同样红,逐类比对 tests/F/E 一致 → 无一条由本单引入 |
|
||||
| 其中 3 类同源 | `fin_reimburse` 列未同步进 `integration/schema.sql`(`#8362` 遗留,finance 域,**不在本单范围**,建议另开单) |
|
||||
|
||||
判据按 `#8370` 立的口径:**「不引入新失败 + 既有失败逐条定责」**,而非「全量全绿」——基底每天在变,小修单担保不了全绿。
|
||||
|
||||
### 本地证据(已完成)
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| `GroupBatchActionControllerTest` | **27/0/0/0**(批 1 的 21 条 + 批 2 新增 6 条) |
|
||||
| 变异①:`transfer-in` 的 MANAGE→VIEW(接错码) | 红 1 条(判「码对不对」那条) |
|
||||
| 变异②:`transfer-candidates` 的 `require` 删掉(漏接) | 红 2 条,含 `expected:<589507> but was:<200>` |
|
||||
| 两次还原 | 均复绿 27/0/0/0,`MUTATION-7608-TEMP` 残留 0 |
|
||||
|
||||
> 批 0 留下的两条「记观察日志但不拦截」用例在本单下**必然会红**,已改写成批 2 形态——这正是它们有分辨力的证明。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue `#7608`(含 58 端点判权清点、FINANCE 三步可达链、批 0 观察窗取证)
|
||||
- 批 0+1:PR `#7616`;AC-9 文档侧:PR `#7661`
|
||||
- 批 2/批 3 判权口径定案:#7608 评论 #60798(wx,2026-09-23)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg
|
||||
@@ -0,0 +1,327 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7608"
|
||||
title: "团期退团提交接新码 group-batch:withdraw:submit(589507,带 nacos 灰度开关);团期核单 finalize 判权前移到幂等分支之前"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "已部署 dev-v3 到 TEST 并实打验证:退团提交七角色矩阵逐格吻合、审批端点角色门对照、nacos 开关 false→还原往返(md5 逐字还原)、核单 finalize 四个非财务角色对已有快照的团期 589507 且不带数据,全程零写入。前端判 not_required(2026-09-27,与批2 同口径守既定架构「可见即可点+589507 兜底、后端不下发 group-batch 按钮码无码可接」):grep 实证 submitGroupBatchWithdraw 无 silentError、消费方 WithdrawSubOrderModal catch 无按码分支,589507 拦截器自动透 message;团级 settlement/finalize 前端零封装零消费(团期核单页 Epic #8361 ③ defer 中);退单入口唯一显隐门为 #8154 isGroupBatchManager;§四 按码控显隐建议同批2 被否,finalize changelog 自述不要求前端改动。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期退团提交收口判权(批 3)+ 核单 finalize 判权前移
|
||||
|
||||
**服务**: hl-order-service-v3(种子在 hl-user-service)
|
||||
**PR**: `#8438`(已合入 `dev-v3`,合并提交 `b8c9b463f`);种子 PR `#8434`(`b95f0b443`,先行上线)
|
||||
**Issue**: #7608
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **`withdraw`(退团提交,GB-ADM-070)从「团期管理员以外的任何后台角色都能调」变为「必须持 `group-batch:withdraw:submit`」,无权限返回 `589507`。** 这是一个**新权限码**,授 `ADMIN` / `CUSTOMIZER`(`SUPER_ADMIN` 短路放行),**不授 `FINANCE`**。
|
||||
|
||||
🔴 **`settlement/finalize`(完成团期核单)对非财务角色一律 `589507`,包括该团期已有核单快照的情况。** 改前判权只在锁内事务层,而「已有快照 → 幂等返回」的分支排在判权之前:快照一旦存在,**任意后台角色调 finalize 都能拿到整份核单数据**,绕过读端点 `reports/group` 的 `group-batch:finance:view`。
|
||||
|
||||
🟢 **两个端点的响应结构、成功码、业务错误码全部不变。** 有权限的调用方行为与改前一致。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`#7455` 自称做了「全端点扫描」,但漏掉 `GroupBatchActionController` 里的 4 个端点(按类计数的审计方法缺陷)。`#7608` 分四批收口:批 0 旁路观察、批 1 `cancel-group`、批 2 `transfer-in` / `transfer-candidates`(均已上线),**本单是批 3**,至此 4 个端点全部收口。
|
||||
|
||||
**为什么 `withdraw` 要单独立码**:退团是「建单 → 审批」两段(`#7100`),审批端点走 `WithdrawApprovalGuard` 角色门,只放 `ADMIN` / `SUPER_ADMIN`。若提交也接 `group-batch:manage`,提交集合与审批集合完全相同;接 `group-batch:view` 又会把提交能力给到 `FINANCE`。故新立 `group-batch:withdraw:submit`,只授真实提交方(jw 2026-09-27 定案)。
|
||||
|
||||
**`finalize` 的缺口是怎么发现的**:本单同时上了构建期门禁 `GroupBatchAdminWriteEndpointPermissionArchTest`(`#7608` AC-7),要求团期 admin 控制器的每个写端点在「自身 / 直接委托的方法及其私有汇聚方法」里看得到判权。它首跑就报出 `finalize` 的判权藏在第二跳。订单级核单的同名编排器 `SettlementFinalizeOrchestrator` 首句就判权,团期版漏了这一句。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 退单户·提交退单申请(GB-ADM-070) | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/withdraw` | 修改 | **新增判权**:需 `group-batch:withdraw:submit`,无权限 `589507`;响应结构与业务错误码不变 |
|
||||
| 2 | 完成团期核单 | POST | `/v3/admin/order/group-batch/:groupBatchId/settlement/finalize` | 修改 | **判权前移**:非 `SUPER_ADMIN` / `ADMIN` / `FINANCE` 在幂等分支也 `589507`;响应结构不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 退单户·提交退单申请 `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/withdraw`
|
||||
|
||||
**VO**: `WithdrawSubOrderReqVO` → `Result<WithdrawSubOrderRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台团期详情「退单」:为某户子订单提交退单申请,只建审批单,执行退团与退款在审批通过时。**本单起需要 `group-batch:withdraw:submit` 权限。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | **行为不变** |
|
||||
| orderId | Path | Long | ✅ | 子订单 ID | **行为不变** |
|
||||
| reason | Body | String | ❌ | ≤512 字 | 退团原因;请求体整体可省略。**行为不变** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `WithdrawSubOrderRespVO` | **结构完全不变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2104057019403218945/sub-order/2104057019403218999/withdraw HTTP/1.1
|
||||
X-Admin-Id: 1001
|
||||
X-Admin-Role: CUSTOMIZER
|
||||
Content-Type: application/json
|
||||
|
||||
{"reason": "客户临时有事"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无分页语义。**降级**:nacos `group-batch.acl.enforce.withdraw-submit` 置 `false` 时,判权**退化为旁路观察日志**(`GB_ACL_PROBE`,`intendedPermission=group-batch:withdraw:submit`)而非静默放行——团期管理员以外的角色恢复放行、与改前一致,但每次调用仍在服务端留痕。开关默认 `true`。团期管理员的 `581008` 不读本开关。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方不持 `group-batch:withdraw:submit` | 🆕 **本端点新增**(码本身早已存在) |
|
||||
| 581008 | `ORDER_VIEW_FORBIDDEN` | 调用方是团期管理员(#8154,排在判权之前) | 不变 |
|
||||
| 589500 / 581007 / 其它业务码 | — | 团期不存在 / 订单不存在 / 各项业务前置 | 不变 |
|
||||
|
||||
`589507` 实打响应体(TEST,2026-09-27,FINANCE):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **拒绝时零写入**:判权在 Controller 入口,排在 Service 任何库读写之前;对不存在的团期也先返回 `589507`。
|
||||
- **提交集合 ≠ 审批集合**:提交 {`SUPER_ADMIN`, `ADMIN`, `CUSTOMIZER`};审批(GB-ADM-072~075,`WithdrawApprovalGuard` 角色门,本单未改){`SUPER_ADMIN`, `ADMIN`}。定制师能提交、不能审批;`FINANCE` 两侧都不在。
|
||||
- **团期管理员**:仍被 `581008` 拦在最前(#8154)。「退单提交放开团期管理员」已另开单跟进,本单不含。
|
||||
|
||||
---
|
||||
|
||||
### 2. 完成团期核单 `POST /v3/admin/order/group-batch/:groupBatchId/settlement/finalize`
|
||||
|
||||
**VO**: `Result<GroupSettlementRespVO>`(无请求体)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期核单页「完成核单」:按团聚合落核单快照;重复提交幂等返回既有快照。只有资金写角色(`SUPER_ADMIN` / `ADMIN` / `FINANCE`)能调,**口径本身不变**,本单只把判权挪到幂等分支之前。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | ≥1 | 团期主键。**行为不变** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `GroupSettlementRespVO` | **结构完全不变**;无权限时为 `null` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/2104008377741012994/settlement/finalize HTTP/1.1
|
||||
X-Admin-Id: 1001
|
||||
X-Admin-Role: FINANCE
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {"finalized": true, "groupSettlementId": "2104009943290146817", "groupBatchId": "2104008377741012994"},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无分页语义,无开关(它本就应判权,不设降级)。已有快照时财务写角色照旧幂等返回同一份快照(TEST 实测 `groupSettlementId` 不变、快照表行数不变)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 非资金写角色,**含已有快照的幂等分支** | 🆕 幂等分支新增;无快照分支改前即为 `589507` |
|
||||
| 其它业务码 | — | 团期不存在、核单前置不满足等 | 不变 |
|
||||
|
||||
`589507` 实打响应体(TEST,2026-09-27,CUSTOMIZER 对已有快照的团期):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **改前的泄露面**:定制师 / 房务 / 车务 / 团期管理员对一个已有核单快照的团期调 `finalize`,改前返回 `200` + 整份核单(团总成本、分摊、人均、预支、应收已收等);改后 `589507`,`data` 为 `null`,且连快照都不查。
|
||||
- 锁内事务层那道判权保留作纵深兜底。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 退单入口建议按 `group-batch:withdraw:submit` 控制可见性;`589507` 直接透出 `message` 即可。
|
||||
- `finalize` 入口的可见性口径不变(资金写角色);本单不要求前端改动。
|
||||
- 权限码与角色绑定**没有运行期写入口**(只能走 Flyway 种子)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- hl-user-service 新增种子 `V20260927_8404_7608__add_group_batch_withdraw_submit_permission.sql`(PR `#8434`):`admin_permission` 插 1 行(新码),`admin_role_permission` 插 2 行(`ADMIN`、`CUSTOMIZER`),`INSERT IGNORE` 幂等,零 DDL。
|
||||
- **上线顺序**:种子 15:28:49 在 TEST 执行(rank 168),过完 10 分钟权限缓存后才部署 order-v3(16:00),避免 `#7154` 那种「非超管全员 589507」。
|
||||
- order-v3 零 DDL、零数据变更。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 有权限的调用方:与改前一致。
|
||||
- 无权限的调用方:`withdraw` 改前进入业务(可建审批单),改后 `589507` 且零写入;`finalize` 见上。
|
||||
- 角色头缺失:`withdraw` 走 `GroupBatchPermissionGuard`,`finalize` 走 `SettlementWriteGuard`,两者对「有请求但角色为空」都 **fail-closed**(`589507`)。
|
||||
- user-service 不可达:权限查询 fail-closed。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 角色 | `withdraw` 改前 → 改后 | `finalize`(已有快照)改前 → 改后 |
|
||||
|------|---|---|
|
||||
| `SUPER_ADMIN` | 正常 → 正常 | 正常 → 正常 |
|
||||
| `ADMIN` | 正常 → 正常 | 正常 → 正常 |
|
||||
| `CUSTOMIZER` | 正常 → 正常(持新码) | **200 + 快照 → 589507** |
|
||||
| `FINANCE` | **正常 → 589507** | 正常 → 正常 |
|
||||
| `ROOM_MANAGER` / `VEHICLE_MANAGER` | **正常 → 589507** | **200 + 快照 → 589507** |
|
||||
| `GROUP_BATCH_MANAGER` | 581008 → 不变 | **200 + 快照 → 589507** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **前端**:`FINANCE`、房务、车务会在退单入口拿到 `589507`;若按码控制可见性则看不到入口。`finalize` 对合法调用方无变化。
|
||||
- **后端**:`withdraw` 每次调用多一跳 Feign 判权。
|
||||
- **回滚**:`withdraw` 改 nacos key 为 `false`,TEST 实测热刷新约 3 秒,不需重发服务;`finalize` 需回滚代码。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 退单审批四端点(GB-ADM-072~075):角色门不变,本单未改。
|
||||
- `cancel-group` / `transfer-in` / `transfer-candidates`(批 1、批 2)与成团 / 流团 / 名额 / 预支:判权口径不变。
|
||||
- 团期核单 `confirm` / `reports/group`、团期归档 `settle` / `reopen-settle`:判权口径与落点不变(新门禁核过均在可见位置)。
|
||||
- 小程序端(`consumer: mp`):本单端点均为管理后台端点,无影响。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-09-27 16:03~16:10
|
||||
**构建身份**:TEST 检出 `dev-v3 @ b8c9b463f`(本单合并提交),order-v3 16:00 重启。零写入判据:旧字节里 `FINANCE` 调 `withdraw` 走旁路观察、会进入业务校验(`589500`),新字节才会 `589507`;旧字节里定制师对已有快照的团期调 `finalize` 返回 `200` + 快照,新字节才会 `589507`。下表两处拦截只可能来自本单字节。
|
||||
|
||||
### 8.1 `withdraw` 七角色矩阵(强制态)
|
||||
|
||||
真实团期 `2104057019403218945` + 不存在的子订单;另用不存在的团期再打一轮,排除「判权排在存在性校验之后」。
|
||||
|
||||
| 角色 | 真实团期 | 不存在的团期 |
|
||||
|---|---|---|
|
||||
| SUPER_ADMIN | `581007` 过守卫 | `589500` 过守卫 |
|
||||
| ADMIN | `581007` 过守卫 | `589500` 过守卫 |
|
||||
| CUSTOMIZER | `581007` 过守卫 | `589500` 过守卫 |
|
||||
| FINANCE | **`589507`** 拦截 | **`589507`** 拦截 |
|
||||
| ROOM_MANAGER | **`589507`** 拦截 | **`589507`** 拦截 |
|
||||
| VEHICLE_MANAGER | **`589507`** 拦截 | **`589507`** 拦截 |
|
||||
| GROUP_BATCH_MANAGER | `581008`(#8154) | `581008`(#8154) |
|
||||
|
||||
审批端点对照(`POST .../withdraw/1/approve`):CUSTOMIZER / FINANCE / GROUP_BATCH_MANAGER → `589530`「仅管理员可处理退单审核」;ADMIN → `589531`「退单申请不存在」(过角色门)。**定制师能提交、不能审批**,两个集合不相等。
|
||||
|
||||
`group_batch_approval` 全程 75 行、最大 id 不变:零写入。**未只用超管自测**:ADMIN 与 CUSTOMIZER 过守卫,证明种子对非超管角色已生效。
|
||||
|
||||
### 8.2 nacos 灰度开关往返
|
||||
|
||||
配置 `hl-order-service-v3-test.yml`(`tenant=test`)原本**不含** `group-batch.*` 键,即跑默认值 `true`。
|
||||
|
||||
| 态 | 配置 | FINANCE | ROOM_MANAGER | CUSTOMIZER | GROUP_BATCH_MANAGER |
|
||||
|---|---|---|---|---|---|
|
||||
| A | 无键(默认 `true`) | `589507` | `589507` | `589500` | `581008` |
|
||||
| B | 追加 `withdraw-submit: false` | `589500`(3.1 秒内生效) | `589500` | `589500` | `581008` |
|
||||
| C | 逐字节还原 | `589507`(2.7 秒内生效) | — | `589500` | — |
|
||||
|
||||
- **还原校验到字节**:发布带 `casMd5`,还原写在 `finally` 里;还原后 `md5` 与原值同为 `c2206934960057f70b7173159046dc54`。
|
||||
- 态 B 下团期管理员仍 `581008`:#8154 守卫不读本开关,符合设计。
|
||||
- 本轮**未取服务端日志**;「关掉退化为旁路观察而非静默放行」由单测 `withdraw_toggleOff_fallsBackToObserverWithNewCode` 钉住(断言观察的是新码)。
|
||||
|
||||
### 8.3 `finalize`(已有快照的团期 `2104008377741012994`)
|
||||
|
||||
| 角色 | 结果 |
|
||||
|---|---|
|
||||
| CUSTOMIZER / ROOM_MANAGER / GROUP_BATCH_MANAGER / VEHICLE_MANAGER | **`589507`**,`data` 为 `null` |
|
||||
| FINANCE | `200`,幂等返回既有快照 `groupSettlementId=2104009943290146817` |
|
||||
|
||||
`order_group_settlement_summary` 前后均 1 行、最大 id 不变:零写入。
|
||||
|
||||
### 本地证据
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| 引用改动点的 17 个测试类 | **267/0/0/0**;合并前按含 `#8437` 的新基底重跑关键 13 类 163/0/0/0 |
|
||||
| `GroupBatchAdminWriteEndpointPermissionArchTest` | 2/0(规则 + 判定分辨力自测:7 阳性 / 5 阴性 / 3 个 `@RequestMapping` 样本) |
|
||||
| 变异:撤掉 `finalize` 编排器入口判权 | 门禁恰好只报 `GroupSettlementController.finalizeSettlement` 一处;编排器新增用例 4 例变红;已还原 |
|
||||
| 既有红 | `WebMvcSliceMockBeanInventoryTest` 1 条,基底同样红(`#8388` 的切片测试类未登记),与本单无关 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue `#7608`;种子 PR `#8434`;批 2 PR `#8416`
|
||||
- `docs/group/团期模块接口文档-v2.0.html` §0A.11(已随本单刷新:GB-ADM-070 行、判定方法第 7 条)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7608](https://git.1814.love/wx/HL/issues/7608)
|
||||
- **PR**: [#8438](https://git.1814.love/wx/HL/pulls/8438)(order-v3)、[#8434](https://git.1814.love/wx/HL/pulls/8434)(user-service 种子)
|
||||
- **Merge commit**: [b8c9b463f](https://git.1814.love/wx/HL/commit/b8c9b463fbb02fc711822bcab6f9d769bd7cc23a)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,492 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8361"
|
||||
title: "团期报销核单「一团一张报账单」——团期核单 finalize/confirm/reports 三端点新增 + 台账应收整团聚合一行 + 报账单 biz 三字段 + 搜索兼容团号 + 核单 summary 新增 SETTLED"
|
||||
consumer: "admin"
|
||||
author: "yst"
|
||||
change_type: "新增接口"
|
||||
backend_status: "merged"
|
||||
gateway_status: "pending"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "9a28a7a8dc12a0c041c616ac3b1000c53847b8a6"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "Epic #8361 五 PR(#8366/#8367/#8368/#8400/#8406)已全部合并 dev-v3,尚未部署测试服、网关未实测。前端需动三处:①应收台账行按 rowType 分流(GROUP_BATCH 行 id 是团期批次 ID,按订单详情跳转必 404);②报账单列表按 bizType 分流展示(GROUP_BATCH 行展示 bizNo 团号);③新增团期核单页(finalize/confirm/reports 三端点 + settlementStatus 新增 SETTLED 态)。【2026-09-27 mmg】①应收台账 rowType 分流 + ②报账单 bizType 分流已交付(276de5f1):GROUP_BATCH 行跳团期详情不跳订单详情、详情「团号」取 bizNo(行快照兜底)、搜索兼容团号文案对齐;receivable/reimburse 两 spec 18 例全绿。③团期核单页 defer——原型快照 Sep 17 早于本 Epic 无其设计(prototype fidelity 不可无原型建财务新页),后端 §11 明说可随后迭代不阻塞,待原型刷新后续做,故 frontend_status 保持 pending。【2026-09-27 mmg ③】团期核单页已交付(1c555f54):用户拍板不算财务域不等原型,详情页「核单结算」后新增「团期核单」tab——reports/group 空壳范式先判 finalized、finalize 节点门 TRIP_FINISHED/REVIEWING 置灰内联、confirm 仅「FINALIZED+待复核」、SETTLED 无人工结清口、金额/ID 全字符串 null 显 —;新建 api/order-v3/groupSettlement.js+GroupSettlementTab.vue,API spec 4 例+组件 spec 10 例+详情页 spec 共 28 例全绿,checkpoint 全绿。Epic ①②③全部闭环,翻 verified。【2026-09-28 mmg ③迁】用户改拍「团期核单归财务域(免原型对齐),finance/settlement 团期产品 tab 即预留位」:先把 order-v2/batch/detail 的「团期核单」Tab 下线(081966ff),再落地财务域(9a28a7a8)——团期产品 tab 数据源由空调常规 /v3/admin/order-settlement/tasks(仅常规 CORE 无团期批次,即"接口不对"根因)改走 GB-ADM-001 /v3/admin/order/group-batch(订单域接口财务只读,pageNo/departFrom/departTo/opsStage 七桶),行弹层挂 GroupSettlementPanel(原 GroupSettlementTab 迁入)读 reports/group+finalize/confirm;应收/已收权威字段直显不反算,未建团行(groupBatchId=null)不放入口。恢复 api/order-v3/groupSettlement.js,新增 index.spec 锁双 tab 数据源+参数名+列 render+弹层入参,三 spec 22 例全绿,checkpoint 全绿。ref 由 1c555f54 改指 9a28a7a8。"
|
||||
updated_at: "2026-09-28"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期报销核单「一团一张报账单」(管理后台)
|
||||
|
||||
**服务**: hl-order-service-v3(团期核单三端点)+ hl-finance(台账应收 / 报账单列表)
|
||||
**Epic**: #8361
|
||||
**PR**: #8366(G1 多态 DDL)+ #8367(G2 三端点)+ #8368(G3 一团一张推送)+ #8400(G4 台账整团聚合/搜索/反向联动)+ #8406(biz 三字段 + keyword 团号)
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口背景
|
||||
|
||||
原核单链路只有「一户一张报账单」(按订单 orderId 聚合)。团期(出团批次)场景下,财务需要按整团做核单、复核、回款跟踪。本次 Epic 把核单与报账单从「一户一张」泛化为「一个业务对象一张」:
|
||||
|
||||
- 团期维度新增「一团一核单」三端点(finalize 核单 → confirm 财务复核并推一团一张报账单 → reports/group 读核单表),快照落库一团一行。
|
||||
- 报账单 fin_reimburse 改 biz_type + biz_id 多态关联:ORDER(一户一张,不变)/ GROUP_BATCH(一团一张,新增)。
|
||||
- 应收台账对团期批次**整团聚合一行**(不按户拆),行上补 rowType/groupBatchId 鉴别字段。
|
||||
- 报账单列表补 bizType/bizId/bizNo 三字段;报账单搜索与台账 keyword 均兼容团号。
|
||||
- 一团一张报账单 RECEIVABLE 全收后,自动反向把团期核单快照 settlementStatus 回写 SETTLED(已结清)。
|
||||
|
||||
## 2. 变更清单
|
||||
|
||||
| # | 变更 | 方法 | 路径 | 类型 | 摘要 |
|
||||
|---|------|------|------|------|------|
|
||||
| 1 | 完成团期核单 | POST | /v3/admin/order/group-batch/{groupBatchId}/settlement/finalize | 新增接口 | 按团聚合落核单快照,幂等 |
|
||||
| 2 | 团期核单财务复核 | POST | /v3/admin/order/group-batch/{groupBatchId}/settlement/confirm | 新增接口 | 复核通过 + 同事务推一团一张报账单 |
|
||||
| 3 | 查询团期核单表 | GET | /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group | 新增接口 | 读 active 快照;未核单返回 finalized=false 空壳 |
|
||||
| 4 | 应收台账分页 | GET | /admin/finance/receipt/receivable/page | 修改接口 | 行新增 rowType/groupBatchId;团期整团聚合一行;keyword 兼容团号 |
|
||||
| 5 | 报账单分页 | GET | /admin/finance/reimburses/page | 修改接口 | 行新增 bizType/bizId/bizNo;orderNo 入参扩为同时匹配团号 biz_no |
|
||||
| 6 | 团期核单 summary | — | (端点 1/2/3 的出参 GroupSettlementRespVO) | 修改接口 | settlementStatus 新增枚举值 SETTLED(全收自动回写,非人工触发) |
|
||||
|
||||
## 3. 接口详情
|
||||
|
||||
| 端点 | 使用场景 | 认证 | 幂等性 | 限流 |
|
||||
|------|---------|------|--------|------|
|
||||
| POST finalize | 团期出行完毕后,核单员按团聚合落核单快照 | 网关 JWT;角色须 SUPER_ADMIN / ADMIN / FINANCE(缺 589507) | **幂等**:重复提交返回既有 active 快照,不重复落库 | 网关统一限流 |
|
||||
| POST confirm | 财务复核团期核单,通过后推一团一张报账单 | 同 finalize | 状态门 + CAS 挡重复复核;推送侧 uk_biz_version(biz_type,biz_id,version_no) 撞号重查兜底返回既有单;推送失败整单回滚(review_status 停 PENDING),**可安全重试** | 网关统一限流 |
|
||||
| GET reports/group | 团期核单表展示(核单前/后都调) | 网关 JWT;须 group-batch:finance:view 权限码(缺 589507) | 只读 | 网关统一限流 |
|
||||
| GET receivable/page | 财务收款-应收台账 | 网关 JWT(finance 域先例,无方法级权限注解) | 只读 | 网关统一限流 |
|
||||
| GET reimburses/page | 报账款列表(复核视角) | 网关 JWT | 只读 | 网关统一限流 |
|
||||
|
||||
**金额/ID 序列化约定**:GroupSettlementRespVO 的所有 BigDecimal 金额字段与 Long ID 字段均序列化为 **JSON 字符串**(防精度丢失);台账行/报账单行的 Long ID 为字符串,BigDecimal 金额为 JSON number。
|
||||
|
||||
## 4. 接口入参
|
||||
|
||||
### 4.1 路径 / Query 参数
|
||||
|
||||
**团期核单三端点**(均无请求体):
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| groupBatchId | path | Long | 是 | 团期 ID,必须 ≥ 1(否则 400「团期 ID 必须大于 0」) |
|
||||
|
||||
**应收台账分页** GET /admin/finance/receipt/receivable/page(query):
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 是 | 页码,从 1 起 |
|
||||
| pageSize | int | 是 | 每页条数 |
|
||||
| keyword | string | 否 | 搜索关键字(**团号 batch_no / 客户姓名 / 产品名 / 订单号** 模糊,任一命中);空=不限。注:只模糊匹配团号本身,不匹配团期名称/期次 |
|
||||
| receivableStatus | string | 否 | UNPAID 待收款 / PARTIAL 部分收款 / DONE 已收讫;空或非法值=不按收款状态过滤 |
|
||||
|
||||
**报账单分页** GET /admin/finance/reimburses/page(query):
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 是 | 页码 |
|
||||
| pageSize | int | 是 | 每页条数 |
|
||||
| status | string | 否 | 单据状态(枚举见 §6);空=全部 |
|
||||
| direction | string | 否 | PAYABLE 公司应付报账人 / RECEIVABLE 报账人应回款 / BALANCED 两清;空=全部 |
|
||||
| orderNo | string | 否 | **订单号/团号**(模糊,同时匹配 order_no 与 biz_no——团期报账单 order_no 为空,按团号 biz_no 也能搜到);空=不限 |
|
||||
| reporterName | string | 否 | 报账人姓名(模糊);空=不限 |
|
||||
|
||||
### 4.2 请求体字段
|
||||
|
||||
三端点均无请求体;两个分页接口均为 GET query 传参,无请求体。
|
||||
|
||||
## 5. 出参字段
|
||||
|
||||
### 5.1 GroupSettlementRespVO(finalize / confirm / reports/group 三端点共用)
|
||||
|
||||
外层统一包装 {"code":200,"success":true,"data":{...}},下表为 data 结构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| finalized | boolean | 是否已有核单快照;false=尚未核单,**快照字段全 null**,只填团期基本信息段(batchNo/productName/departDate/batchStatus),不抛 404 |
|
||||
| groupSettlementId | string(Long) | 团期核单快照 ID |
|
||||
| groupBatchId | string(Long) | 团期 ID |
|
||||
| batchNo | string | 团期号,例 20261001-01 |
|
||||
| productName | string | 产品名称快照 |
|
||||
| departDate | string(date) | 出发日期 yyyy-MM-dd |
|
||||
| batchStatus | string | 团期状态(GroupBatchStatus 码,见 §6) |
|
||||
| settledOrderCount | int | 已核单子订单户数 |
|
||||
| totalActiveOrderCount | int | 在团子订单总户数(仅排除已取消) |
|
||||
| subOrderTotalActualCost | string(BigDecimal) | 子订单实际成本合计 |
|
||||
| subOrderTotalProfit | string(BigDecimal) | 子订单毛利合计 |
|
||||
| sharedCostTotal | string(BigDecimal) | 团期共享成本合计 |
|
||||
| sharedCostByType | array | 共享成本按类型汇总,元素见下表 |
|
||||
| grandTotalCost | string(BigDecimal) | 团期总成本(子订单成本合计 + 共享成本合计) |
|
||||
| actualTravelerCount | int | 实际出行人数 |
|
||||
| perPersonSharedCost | string(BigDecimal) 或 null | 人均共享成本;出行人数为 0 时为 null |
|
||||
| groupAdvanceApproved | string(BigDecimal) | 整团预支已批合计(scope=GROUP_BATCH;资金拨付提示,不计入 grandTotalCost) |
|
||||
| groupAdvancePending | string(BigDecimal) | 整团预支待批合计(仅提示,不参与成本/毛利公式) |
|
||||
| groupTotalAmount | string(BigDecimal) | 全团应收总额(一团一张报账单净额输入) |
|
||||
| groupPaidAmount | string(BigDecimal) | 全团已付总额 |
|
||||
| reviewStatus | string | 复核状态:PENDING 待复核 / APPROVED 已复核 / RETURNED 已退回 |
|
||||
| settlementStatus | string | 核单状态:PENDING 待核单 / FINALIZED 已核单 / **SETTLED 已结清(新增,见 §6)** |
|
||||
| flowStatus | string | 团期流程状态快照:TRIP_FINISHED / REVIEWING / SETTLED(confirm 通过后翻 SETTLED;独立于团期本体状态机,不动 order_group_batch.batch_status) |
|
||||
| settledBy | string(Long) | 核单人 ID |
|
||||
| settledByName | string | 核单人姓名快照 |
|
||||
| settledAt | string(datetime) | 团核单完成时间 yyyy-MM-dd HH:mm:ss |
|
||||
| remark | string | 备注 |
|
||||
| createTime | string(datetime) | 快照创建时间 |
|
||||
|
||||
**sharedCostByType[] 元素(SharedCostByTypeVO)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| costType | string | 成本类型码:BUS 整团大巴 / LEADER 领队 / PHOTOGRAPHER 摄影 / OTHER 其他 |
|
||||
| costTypeDesc | string | 成本类型中文标签 |
|
||||
| total | string(BigDecimal) | 该类型成本合计 |
|
||||
|
||||
### 5.2 应收台账行 ReceiptReceivableRowRespVO(receivable/page 的 data.records[])
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| **rowType** | string | **新增**。ORDER 订单行 / GROUP_BATCH 团期整团聚合行。**行点击/跳转策略必须按本字段分流** |
|
||||
| id | string(Long) | rowType=ORDER 时为订单 ID(可跳订单详情);**rowType=GROUP_BATCH 时挂团期批次 ID(与 groupBatchId 同值),按订单 ID 跳详情必 404** |
|
||||
| **groupBatchId** | string(Long) 或 null | **新增**。rowType=GROUP_BATCH 时有值;ORDER 行恒 null |
|
||||
| orderNo | string | rowType=ORDER 为订单号;GROUP_BATCH 行挂团号 batch_no |
|
||||
| teamNo | string | 团号;GROUP_BATCH 行同为团号 batch_no |
|
||||
| productName | string | 产品名(GROUP_BATCH 行为团期名) |
|
||||
| customerName | string 或 null | 客户姓名;**GROUP_BATCH 团行恒 null(整团无单一客户)** |
|
||||
| customerPhone | string 或 null | 客户手机号(明文,内部财务对账用);**GROUP_BATCH 团行恒 null** |
|
||||
| orderStatus | string | rowType=ORDER 为 OrderStatus 枚举值;**rowType=GROUP_BATCH 为 GroupBatchStatus 码——两套枚举空间按 rowType 分流** |
|
||||
| orderStatusName | string | 上述状态的中文名 |
|
||||
| receivableStatus | string | UNPAID 待收款 / PARTIAL 部分收款 / DONE 已收讫 |
|
||||
| receivableStatusName | string | 收款状态中文名 |
|
||||
| receivableAmount | number | 应收总额(取消单归 0)。团行:已核单团读快照全团应收;未核单团按在团子订单实时求和 |
|
||||
| paidAmount | number | 已收金额(毛额,退款不回减)。团行结清(SETTLED)后顶满应收 |
|
||||
| collectedAmount | number | 代收金额。已核单团的未结清欠收挂本列(一团一张报账单已推送,全团尾款=主报账人代收口径);未核单团恒 0 |
|
||||
| balanceAmount | number | 欠收金额。与 collectedAmount 互斥、合计恒等于实时待收尾款;团行结清后归 0 |
|
||||
|
||||
**团期整团聚合行为(新增)**:页内命中的团期子订单按 groupBatchId 折叠——**每团只出一行台账**,同团其余子订单行丢弃,total 按页内折叠数修正。已知近似(后端拍板接受):折叠在页内做,同团子订单跨页时该团在每页各出一行;DONE 筛选视图下 total 为近似值,跨页行数/total 可能有偏差。
|
||||
|
||||
### 5.3 报账单行 ReimburseRowRespVO(reimburses/page 的 data.records[])
|
||||
|
||||
只列本次**新增**三字段(其余字段不变:id/reimburseNo/reporterName/reporterRole/direction/collectableAmount/actualCollectedAmount/underCollectAmount/advanceAmount/cashPaidAmount/netAmount/settleAmount/receivedAmount/remainingAmount/status/reviewByName/reviewTime/createTime):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| **bizType** | string | **新增**。ORDER 订单单(一户一张)/ GROUP_BATCH 团期单(一团一张);前端按本字段分流展示——GROUP_BATCH 行展示 bizNo(团号) |
|
||||
| **bizId** | string(Long) | **新增**。bizType=ORDER 为订单 ID;GROUP_BATCH 为团期批次 ID(**勿按订单 ID 跳详情**) |
|
||||
| **bizNo** | string | **新增**。bizType=ORDER 为订单号;GROUP_BATCH 为团号 batch_no |
|
||||
|
||||
既有字段语义修订:
|
||||
|
||||
| 字段 | 修订 |
|
||||
|------|------|
|
||||
| orderId | bizType=GROUP_BATCH 团期单**恒 null** |
|
||||
| orderNo | bizType=GROUP_BATCH 团期单**恒 null**,团号看 bizNo |
|
||||
|
||||
## 6. 枚举 / 数据字典
|
||||
|
||||
**rowType(台账行类型,新增)**:ORDER 订单行 / GROUP_BATCH 团期整团聚合行。
|
||||
|
||||
**bizType(报账单业务对象类型,新增)**:ORDER 订单单(一户一张)/ GROUP_BATCH 团期单(一团一张)。
|
||||
|
||||
**settlementStatus(团期核单状态,本次新增 SETTLED)**:
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| PENDING | 待核单 | 快照未 finalize(reports/group 空壳态不出现本值,看 finalized=false) |
|
||||
| FINALIZED | 已核单 | finalize 完成,待财务复核 |
|
||||
| **SETTLED** | **已结清** | **新增**。一团一张报账单 RECEIVABLE 全收后由后端监听端自动回写(FINALIZED→SETTLED),**非任何人工端点触发**;confirm 不会把 settlementStatus 翻成 SETTLED(confirm 翻的是 reviewStatus 与 flowStatus) |
|
||||
|
||||
**reviewStatus(复核状态)**:PENDING 待复核 / APPROVED 已复核 / RETURNED 已退回。
|
||||
|
||||
**flowStatus(快照内流程状态)**:TRIP_FINISHED / REVIEWING / SETTLED。
|
||||
|
||||
**GroupBatchStatus(团期状态,GROUP_BATCH 台账行的 orderStatus 与核单快照 batchStatus 用)**:RECRUITING 招募中 / RESOURCE_PREPARING 资源准备中 / MATERIAL_PREPARING 物料准备中 / PENDING_DEPARTURE 待出发 / TRAVELLING 出行中 / TRIP_FINISHED 出行完毕 / REVIEWING 核单中 / SETTLED 已结算 / CANCELLED 已取消。
|
||||
|
||||
**OrderStatus(订单粗状态,ORDER 台账行的 orderStatus 用)**:PENDING_PAY 待支付 / CUSTOMIZING 定制中 / PENDING_DEPARTURE 待出行 / TRAVELLING 出行中 / COMPLETED 已完成 / CANCELLED 已取消。
|
||||
|
||||
**receivableStatus(收款状态)**:UNPAID 待收款 / PARTIAL 部分收款 / DONE 已收讫。
|
||||
|
||||
**报账单 status**:PENDING 待复核 / APPROVED 已批准 / PARTIAL_RECEIVED 部分收讫 / PAID 已付讫 / RECEIVED 已回款 / CLOSED 两清 / RETURNED 已退回。
|
||||
|
||||
**direction(报账方向)**:PAYABLE 公司应付报账人 / RECEIVABLE 报账人应回款 / BALANCED 两清。
|
||||
|
||||
**costType(共享成本类型)**:BUS 整团大巴 / LEADER 领队 / PHOTOGRAPHER 摄影 / OTHER 其他。
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
外层结构 {"code":错误码,"message":"文案","data":null,"success":false}。
|
||||
|
||||
| 错误码 | 触发端点 | 文案({0} 为占位) | 处置建议 |
|
||||
|--------|---------|-------------------|---------|
|
||||
| 400 | 三端点 | 团期 ID 必须大于 0 | 参数校验失败 |
|
||||
| 589500 | 三端点 | 团期不存在 | 检查 groupBatchId |
|
||||
| 589507 | 三端点 | 无操作权限(当前角色未授予团期权限,或该团期不在您名下) | finalize/confirm 需 SUPER_ADMIN/ADMIN/FINANCE;reports/group 需 group-batch:finance:view |
|
||||
| 584133 | finalize | 该团期未配置主报账人,请先在团期人员配置中设置主报账人后再核单 | 去团期人员配置设主 |
|
||||
| 584134 | finalize | 该团期存在 {0} 个主报账人,一团只能有一个主报账人,请先去重后再核单 | 去重主报账人 |
|
||||
| 584135 | finalize | 团期子订单 {0} 尚未确认行程,请全团子订单都确认行程后再核单({0}=子订单 ID) | 催该户确认行程 |
|
||||
| 584136 | confirm | 该团期尚未完成核单,请先完成核单后再做财务复核 | 先 finalize |
|
||||
| 584137 | confirm | 当前团期核单状态不允许财务复核(须为已核单且待复核),操作已回滚 | 刷新快照状态;并发复核败者也会命中 |
|
||||
| 584138 | finalize | 团期当前状态为「{0}」,未到核单节点(需出行完毕或核单中),不允许核单({0}=当前 batchStatus) | 等团期到 TRIP_FINISHED/REVIEWING |
|
||||
|
||||
## 8. 示例
|
||||
|
||||
### 8.1 典型成功——finalize 后读团期核单表
|
||||
|
||||
POST /v3/admin/order/group-batch/2097250563497385985/settlement/finalize(无请求体)→ 响应与 GET reports/group 同构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"finalized": true,
|
||||
"groupSettlementId": "9101234567890123456",
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchNo": "20261001-01",
|
||||
"productName": "呼伦贝尔草原 5 日游",
|
||||
"departDate": "2026-10-01",
|
||||
"batchStatus": "REVIEWING",
|
||||
"settledOrderCount": 5,
|
||||
"totalActiveOrderCount": 6,
|
||||
"subOrderTotalActualCost": "45600.00",
|
||||
"subOrderTotalProfit": "8400.00",
|
||||
"sharedCostTotal": "3200.00",
|
||||
"sharedCostByType": [
|
||||
{"costType": "BUS", "costTypeDesc": "整团大巴", "total": "2000.00"},
|
||||
{"costType": "LEADER", "costTypeDesc": "领队", "total": "1200.00"}
|
||||
],
|
||||
"grandTotalCost": "48800.00",
|
||||
"actualTravelerCount": 18,
|
||||
"perPersonSharedCost": "177.78",
|
||||
"groupAdvanceApproved": "5000.00",
|
||||
"groupAdvancePending": "0.00",
|
||||
"groupTotalAmount": "54000.00",
|
||||
"groupPaidAmount": "48000.00",
|
||||
"reviewStatus": "PENDING",
|
||||
"settlementStatus": "FINALIZED",
|
||||
"flowStatus": "REVIEWING",
|
||||
"settledBy": "40001",
|
||||
"settledByName": "王财务",
|
||||
"settledAt": "2026-09-27 12:00:00",
|
||||
"remark": "团核单",
|
||||
"createTime": "2026-09-27 12:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
台账分页典型响应(GET /admin/finance/receipt/receivable/page?page=1&pageSize=20&keyword=20261001):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"rowType": "ORDER",
|
||||
"id": "2097000000000000001",
|
||||
"groupBatchId": null,
|
||||
"orderNo": "HL20260920001",
|
||||
"teamNo": "T-0920-01",
|
||||
"productName": "阿尔山 3 日游",
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800001111",
|
||||
"orderStatus": "COMPLETED",
|
||||
"orderStatusName": "已完成",
|
||||
"receivableStatus": "PARTIAL",
|
||||
"receivableStatusName": "部分收款",
|
||||
"receivableAmount": 6000.00,
|
||||
"paidAmount": 3000.00,
|
||||
"collectedAmount": 0,
|
||||
"balanceAmount": 3000.00
|
||||
},
|
||||
{
|
||||
"rowType": "GROUP_BATCH",
|
||||
"id": "2097250563497385985",
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"orderNo": "20261001-01",
|
||||
"teamNo": "20261001-01",
|
||||
"productName": "呼伦贝尔草原 5 日游",
|
||||
"customerName": null,
|
||||
"customerPhone": null,
|
||||
"orderStatus": "SETTLED",
|
||||
"orderStatusName": "已结算",
|
||||
"receivableStatus": "DONE",
|
||||
"receivableStatusName": "已收讫",
|
||||
"receivableAmount": 54000.00,
|
||||
"paidAmount": 54000.00,
|
||||
"collectedAmount": 0,
|
||||
"balanceAmount": 0
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 边界——未核单团读核单表(finalized=false 空壳)+ 报账单 GROUP_BATCH 行
|
||||
|
||||
GET reports/group(该团尚未 finalize)——**不抛 404**,快照字段全 null,前端展示「尚未核单」占位:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"finalized": false,
|
||||
"groupSettlementId": null,
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchNo": "20261001-01",
|
||||
"productName": "呼伦贝尔草原 5 日游",
|
||||
"departDate": "2026-10-01",
|
||||
"batchStatus": "TRIP_FINISHED",
|
||||
"settledOrderCount": null,
|
||||
"totalActiveOrderCount": null,
|
||||
"subOrderTotalActualCost": null,
|
||||
"subOrderTotalProfit": null,
|
||||
"sharedCostTotal": null,
|
||||
"sharedCostByType": null,
|
||||
"grandTotalCost": null,
|
||||
"actualTravelerCount": null,
|
||||
"perPersonSharedCost": null,
|
||||
"groupAdvanceApproved": null,
|
||||
"groupAdvancePending": null,
|
||||
"groupTotalAmount": null,
|
||||
"groupPaidAmount": null,
|
||||
"reviewStatus": null,
|
||||
"settlementStatus": null,
|
||||
"flowStatus": null,
|
||||
"settledBy": null,
|
||||
"settledByName": null,
|
||||
"settledAt": null,
|
||||
"remark": null,
|
||||
"createTime": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
报账单列表中的团期单(GET /admin/finance/reimburses/page?page=1&pageSize=20&orderNo=20261001,按团号搜到)——orderId/orderNo 恒 null,团号看 bizNo:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "9123456789012345678",
|
||||
"reimburseNo": "BZ-20260927-0001",
|
||||
"orderId": null,
|
||||
"orderNo": null,
|
||||
"bizType": "GROUP_BATCH",
|
||||
"bizId": "2097250563497385985",
|
||||
"bizNo": "20261001-01",
|
||||
"reporterName": "李领队",
|
||||
"reporterRole": "GUIDE",
|
||||
"direction": "RECEIVABLE",
|
||||
"collectableAmount": 54000.00,
|
||||
"actualCollectedAmount": 48000.00,
|
||||
"underCollectAmount": 6000.00,
|
||||
"advanceAmount": 0,
|
||||
"cashPaidAmount": 0,
|
||||
"netAmount": 6000.00,
|
||||
"settleAmount": 6000.00,
|
||||
"receivedAmount": 0,
|
||||
"remainingAmount": 6000.00,
|
||||
"status": "APPROVED",
|
||||
"reviewByName": "王财务",
|
||||
"reviewTime": "2026-09-27 12:30:00",
|
||||
"createTime": "2026-09-27 12:30:00"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 业务失败——confirm 时团期未核单(584136)
|
||||
|
||||
POST confirm(该团尚未 finalize):
|
||||
|
||||
```json
|
||||
{"code": 584136, "message": "该团期尚未完成核单,请先完成核单后再做财务复核", "data": null, "success": false}
|
||||
```
|
||||
|
||||
finalize 时团期状态未到核单节点(584138,{0} 透出当前状态):
|
||||
|
||||
```json
|
||||
{"code": 584138, "message": "团期当前状态为「TRAVELLING」,未到核单节点(需出行完毕或核单中),不允许核单", "data": null, "success": false}
|
||||
```
|
||||
|
||||
## 9. 业务边界
|
||||
|
||||
**适用**:
|
||||
- finalize:团期状态须为 TRIP_FINISHED(出行完毕)或 REVIEWING(核单中);全团恰好 1 个 PRIMARY 主报账人;全团在团子订单(不含已取消)均已确认行程。
|
||||
- confirm:须已存在 active 核单快照且为「FINALIZED + 待复核 PENDING」。
|
||||
- 台账/报账单列表的 GROUP_BATCH 行:仅团期批次订单(有 groupBatchId 关联)才会聚合/出现;散单(ORDER 行)行为完全不变。
|
||||
|
||||
**不适用**:
|
||||
- 招募中~出行中的团期不允许 finalize(584138)——共享成本未录、子订单成本未发生,快照无意义。
|
||||
- 未 finalize 的团期不允许 confirm(584136)。
|
||||
- SETTLED / CANCELLED 团期同样不允许 finalize(584138,fail-closed 闭集)。
|
||||
|
||||
**特殊边界**:
|
||||
- 重复 finalize:幂等返回既有快照,不重复落库、不报错。
|
||||
- 并发 confirm:CAS 败者报 584137,不静默成功。
|
||||
- confirm 推送报账单失败:整单回滚(review_status 停 PENDING),可安全重试;推送幂等由 uk_biz_version 兜底。
|
||||
- settlementStatus=SETTLED 只能由「一团一张报账单 RECEIVABLE 全收」自动回写,无人工端点能置该态;部分回款不触发。
|
||||
- 团期回款不回写子订单 paid_amount:台账团行已核单后金额读团快照,子订单级欠收停留在订金态属预期,全团回款进度由报账单承接。
|
||||
|
||||
## 10. 修改前后对比
|
||||
|
||||
### 字段级
|
||||
|
||||
| 接口 | 字段 | 原来 | 现在 |
|
||||
|------|------|------|------|
|
||||
| receivable/page 行 | rowType | 无此字段 | 新增,ORDER / GROUP_BATCH |
|
||||
| receivable/page 行 | groupBatchId | 无此字段 | 新增;GROUP_BATCH 行有值,ORDER 行恒 null |
|
||||
| receivable/page 行 | id | 恒为订单 ID | GROUP_BATCH 行挂团期批次 ID(勿跳订单详情) |
|
||||
| receivable/page 行 | customerName / customerPhone | 恒有值 | GROUP_BATCH 行恒 null |
|
||||
| receivable/page 行 | orderStatus / orderStatusName | 恒为 OrderStatus 枚举 | GROUP_BATCH 行为 GroupBatchStatus 码(两套枚举按 rowType 分流) |
|
||||
| receivable/page 入参 | keyword | 团号(散团 team_no)/客户姓名/产品名/订单号 四字段模糊 | 追加兼容**团期团号 batch_no**(命中的团整团进入结果集并折叠成 GROUP_BATCH 行) |
|
||||
| reimburses/page 行 | bizType / bizId / bizNo | 无此三字段 | 新增;GROUP_BATCH 行 orderId/orderNo 恒 null |
|
||||
| reimburses/page 入参 | orderNo | 仅模糊匹配 order_no | 同时模糊匹配 order_no 与 **biz_no**(团号可搜到团期单) |
|
||||
| GroupSettlementRespVO | settlementStatus | PENDING / FINALIZED | 追加 **SETTLED**(全收自动回写) |
|
||||
|
||||
### 行为级
|
||||
|
||||
| 行为 | 原来 | 现在 |
|
||||
|------|------|------|
|
||||
| 台账团期订单展示 | 逐户一行(团期子订单每户一行台账) | 整团聚合一行(页内折叠,total 按折叠数修正) |
|
||||
| 台账行数/total | 订单粒度精确计数 | 团折叠后 total 为近似值(跨页折叠 + DONE 视图粗筛假阳性剔除,后端拍板接受) |
|
||||
| 报账单生成 | 只有订单核单「一户一张」 | 团期 confirm 追加「一团一张」(biz_type=GROUP_BATCH,单号同为 BZ- 前缀) |
|
||||
| 报账单 biz 关联 | order_id 单列 | biz_type + biz_id 多态(ORDER 单 biz 三字段与旧 order 字段同值) |
|
||||
|
||||
## 11. 影响评估 / 回滚
|
||||
|
||||
**破坏兼容评估**:
|
||||
- 新增端点(finalize/confirm/reports/group):纯新增,无兼容风险。
|
||||
- 台账/报账单列表新增字段:纯追加,旧前端忽略新字段可继续工作。
|
||||
- **行为级不兼容(唯一必须前端同步的点)**:台账对团期子订单**不再逐户出行**。旧前端若把每行当订单、按 id 跳订单详情,GROUP_BATCH 行会 404;且团期子订单行数变少。旧前端没有团期入口时感知为「团期订单从台账消失、多了一个点不开的行」。
|
||||
|
||||
**前端同步上线建议**:台账 rowType 分流与报账单 bizType 分流随后端同窗口上线;团期核单页(三端点)可随后迭代,不阻塞。
|
||||
|
||||
**回滚方案**:后端回滚 #8406/#8400/#8368/#8367/#8366 五个合并提交(逆序)。回滚后台账恢复逐户一行、新增字段消失;已落库的 GROUP_BATCH 报账单与团期核单快照保留在库但不再被读写。前端对新增字段若做了「有则展示」的容错,回滚零改动;若强依赖 rowType 分流,回滚后分流逻辑永不命中 GROUP_BATCH 分支,自然降级为 ORDER 行为。
|
||||
|
||||
## 12. 注意事项
|
||||
|
||||
1. **rowType 分流是硬要求**:GROUP_BATCH 行的 id 是团期批次 ID(与 groupBatchId 同值),**严禁**当订单 ID 跳订单详情(必 404)。
|
||||
2. **两套状态枚举空间**:台账行 orderStatus 在 GROUP_BATCH 行是 GroupBatchStatus 码(9 态)、ORDER 行是 OrderStatus 码(6 态),翻译展示必须按 rowType 选字典,不可混用(两边都有 TRAVELLING/SETTLED/CANCELLED 等同名值但语义层级不同)。
|
||||
3. **金额与 ID 序列化**:GroupSettlementRespVO 的 BigDecimal 金额与 Long ID 全是 JSON 字符串;台账行/报账单行 Long ID 是字符串、金额是 number。
|
||||
4. **keyword 只匹配团号本身**(batch_no LIKE),不匹配团期名称/期次;台账团行上的标识就是团号(orderNo/teamNo 同挂 batch_no)。
|
||||
5. **SETTLED 无人工触发口**:settlementStatus=SETTLED 只由回款全收自动回写;前端不要提供「手动结清」按钮,也不要在 confirm 后期望 settlementStatus 立即变 SETTLED(confirm 翻的是 reviewStatus/flowStatus)。
|
||||
6. **空壳范式**:reports/group 用 finalized=false + 全 null 表示未核单,不是 404;前端先判 finalized 再渲染快照区。
|
||||
7. **报账单列表 GROUP_BATCH 行** orderId/orderNo 恒 null,展示层一律取 bizNo;报账单详情接口 GET /admin/finance/reimburses/{id} 路径不变、按报账单 id 查,两种 bizType 通用。
|
||||
8. 团期回款不回写子订单 paid_amount,勿因「子订单仍显示欠收」报 bug——全团回款进度看报账单与台账团行。
|
||||
|
||||
## 13. 关联 / 联系人
|
||||
|
||||
- Epic:https://git.1814.love/wx/HL/issues/8361
|
||||
- 子 Issue:https://git.1814.love/wx/HL/issues/8362(G1)/ https://git.1814.love/wx/HL/issues/8363(G2)/ https://git.1814.love/wx/HL/issues/8364(G3)/ https://git.1814.love/wx/HL/issues/8365(G4)/ https://git.1814.love/wx/HL/issues/8402(biz 三字段 + keyword 团号)
|
||||
- PR:https://git.1814.love/wx/HL/pulls/8366 / https://git.1814.love/wx/HL/pulls/8367 / https://git.1814.love/wx/HL/pulls/8368 / https://git.1814.love/wx/HL/pulls/8400 / https://git.1814.love/wx/HL/pulls/8406
|
||||
- Commit:https://git.1814.love/wx/HL/commit/97e1736ca6 / https://git.1814.love/wx/HL/commit/355c578d1b / https://git.1814.love/wx/HL/commit/1f65d78941 / https://git.1814.love/wx/HL/commit/a7d41a3b22 / https://git.1814.love/wx/HL/commit/811f3a2fc4
|
||||
- 后端负责人:腰苏图(yst)
|
||||
@@ -0,0 +1,557 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8401"
|
||||
title: "房务配房台账联动:确认缺团号拒绝、清空/改价审计应付行"
|
||||
consumer: "admin"
|
||||
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: "PR #8402 已合入 dev-v3(合并提交 57199b539);2026-09-27 部署测试环境并经网关实测 AC-1 至 AC-4;pre-flight 核对部署 COMMIT=57199b539 STATE=ok【2026-09-27 mmg】前端判 not_required(零代码):808188/599602 grep 前端零引用无错误码字典;五个配房写口全走标准 http 封装无 silentError,消费方三 composable catch 均由 request.js 拦截器统一弹 message(该拦截器对 4xxxxx/5xxxxx 全业务码透 body.message);改价/删除改前本就抛 599600/599601 前端零按码处理,599xxx 同路;§6.7 自述「前端 100% 透明」与实证一致,建议文案「请先完成收款/联系财务解冻」是 UX 建议非契约,后端 message 已可达因。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 房务配房台账联动:确认前置关卡、改价/清空应付行审计
|
||||
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8402 | **Issue**: #8401 | **合并提交**: `57199b539`
|
||||
> **影响范围**: 管理后台「房务配房工作台 → 配房确认、改价、删除、清空」、应付款台账行联动
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**确认配房前新增团号检查;改价、删除、清空配房时对应付款台账行作同事务作废与冻结校验。**
|
||||
|
||||
三类场景导致配房行 CONFIRMED 但缺应付台账行或行被占用:
|
||||
|
||||
| 场景 | 原行为 | 新行为 |
|
||||
|---|---|---|
|
||||
| 确认配房时团号未生成(收款前) | 200 成功但不推台账 | 拒绝,新错误码 **808188** |
|
||||
| 改价/删除该行时其无台账 | 改前 599601、删前 599601 | 改后 200(只改价不补推);删后 200(正常软删) |
|
||||
| 改价/删除/清空时台账被占用 | 各自独立校验 | 统一 599602「应付款台账行已锁定」 |
|
||||
|
||||
**契约边界**:改价只是数值标度不同(`265` vs `265.00`)不再视作改价,应付行不作废。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
房务配房成立应付款「该付供应商的钱」的凭据,必须在确认时同事务落应付款台账行。多轮测试发现三处漏洞:
|
||||
|
||||
1. **无团号可确认**:订单未收款时 team_no 为空,确认应该被拒,原实现放行确认但不推台账,留下「CONFIRMED 但无台账」。
|
||||
2. **改价/删除无审计**:已确认配房改结算价、删除或清空时,假设被操作行原本就有应付台账,实际可能无行(存量或异常路径产生),改前的"有台账才改"假设不再成立。
|
||||
3. **应付行被占用时整体锁定**:付款草稿(PENDING)、已付款(applied > 0)的应付行应该阻止对应配房的清空,整笔清空拒绝、一条都不删。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 单日确认配房 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm` | **行为变更** | 订单 team_no 为空时拒绝,新错误码 808188;配房行保持 INQUIRING,不生成应付台账 |
|
||||
| 2 | 修改单条配房 | PUT | `/v3/admin/order/assignments/{id}` | **副作用变更** | 改结算价时,原无应付台账行的只改价不补推;原有台账行的作废旧行按新价重推;只是标度不同不视作改价 |
|
||||
| 3 | 删除配房 | DELETE | `/v3/admin/order/assignments/{id}` | **副作用变更** | 已确认但无应付台账行的配房删除不再报 599601,正常 200 软删 |
|
||||
| 4 | 清空整需求配房 | DELETE | `/v3/admin/order/hotel-requirements/{requirementId}/assignments` | **校验变更** | 清空时同步作废已确认配房的应付台账行;台账行被占用时整体拒绝 599602 |
|
||||
| 5 | 清空某天配房 | DELETE | `/v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}` | **校验变更** | 同接口 4,按日分流 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 单日确认配房 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm`
|
||||
|
||||
**VO**: `AssignmentDayConfirmReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务配房工作台选择该天候选配房后点「确认」,调用该接口保留选中行、软删落选行、推送应付款台账。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `requirementId` | Path | Long | ✅ | — | 房型需求 id |
|
||||
| `dayNumber` | Path | Integer | ✅ | ≥1 | 第几晚(从 1 开始)|
|
||||
| `keepAssignmentIds` | Body | List<Long> | ❌ | 非空时每个 id 须属于该天候选 | 要保留确认的配房行 id;缺省为空(保留全部)|
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Null | 无返回体 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"keepAssignmentIds": [2104045881147965442]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
确认成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
入参 `keepAssignmentIds` 为空时视为保留该天全部候选,该天无候选时返回 808118:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808118,
|
||||
"message": "该天无可确认的询房中候选, 请先配房再确认",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**新增错误码 808188**:订单团号未生成(收款前)时拒绝确认:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808188,
|
||||
"message": "订单团号未生成(收款后生成),暂不能确认配房",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
保留的配房行不属于该天候选时 fail-fast 拒绝:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 808123,
|
||||
"message": "保留的配房行不属于该天询房中候选(可能配房已更新), 请刷新后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **团号检查**:CORE 订单检查 `order_main.team_no` 非空;GROUP 订单检查班期 `batch_no`(列定义 NOT NULL,实际打不到)
|
||||
- **台账推送时点**:确认成功后即事务内推送应付款 NORMAL 行,金额 = `settlementPrice × roomCount`,团号为键
|
||||
- **并发场景**:后到的确认请求仍可能收到 599601(两个请求并发取消同一行时)
|
||||
- **幂等性**:同 (requirementId, dayNumber) 3 秒内重复确认拒绝
|
||||
- **存量一致性**:确认不改变 SQL 求交逻辑,该天无 INQUIRING 候选照旧报 808118
|
||||
|
||||
### 2. 修改单条配房 `PUT /v3/admin/order/assignments/{id}`
|
||||
|
||||
**VO**: `AssignmentUpdateReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务工作台配房行改价/备注/支付方式时调用;路径参数、出参结构、签名、权限均未变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | Long | ✅ | — | 配房行 id(`house_hotel_assignment.id`)|
|
||||
| `protoPrice` | Body | BigDecimal | ❌ | ≥0 | 原始供应商报价 |
|
||||
| `settlementPrice` | Body | BigDecimal | ❌ | ≥0 | 结算价(现在改后作废旧台账行)|
|
||||
| `settlementMethod` | Body | String | ❌ | 枚举值 | 支付方式 |
|
||||
| `remark` | Body | String | ❌ | — | 备注 |
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Null | 无返回体 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
改结算价:
|
||||
|
||||
```json
|
||||
{
|
||||
"settlementPrice": "380.00",
|
||||
"remark": "协议优惠"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
改价成功(原无应付台账行,只改价不补推):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无特殊空数据;行不存在或权限不足时返回对应业务码。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
应付款台账行已被占用(付款申请占用或已付款):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 599602,
|
||||
"message": "应付款台账行已锁定",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **改价判定**:值的数值部分不同才视作改价(`265.00` 与 `265.000` 视同不改),例如 `380.00` → `390.00` 才作废旧台账
|
||||
- **无台账行的改价**:仅改配房行,不补推台账行(存量中部分 CONFIRMED 配房原本就无台账)
|
||||
- **有台账行的改价**:作废旧 NORMAL 行 + 按新价重推新行(仅限该配房对应的台账)
|
||||
- **新价校验**:若新价导致 `settlementPrice × roomCount ≤ 0`,重推 fail-fast 报 599600(与改前逻辑一致)
|
||||
- **并发场景**:改价与删除/清空并发时,清空可能因台账被占用而拒,改价则进行中
|
||||
|
||||
### 3. 删除配房 `DELETE /v3/admin/order/assignments/{id}`
|
||||
|
||||
**VO**: `StaffAssignmentVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务工作台删除单条配房行;路径参数、出参结构、签名、权限均未变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `id` | Path | Long | ✅ | — | 配房行 id |
|
||||
|
||||
#### 出参 `Result<Void>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | Null | 无返回体 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/assignments/2100873375659266050
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
删除成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
行不存在或权限不足时返回 808120 或 808110。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
应付款台账行已被占用:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 599602,
|
||||
"message": "应付款台账行已锁定",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **已确认无台账的删除**:改前报 599601,改后正常 200 软删(存量或异常路径产生的 CONFIRMED 无台账行)
|
||||
- **已确认有台账的删除**:行被占用时报 599602,阻止删除;未被占用时正常软删并同步作废应付行
|
||||
- **幂等性**:同一行删除后重删报 808120(行已软删、再读不到)
|
||||
- **与清空互斥**:单行删除与整需求/整天清空互斥,串行化执行
|
||||
|
||||
### 4. 清空整需求配房 `DELETE /v3/admin/order/hotel-requirements/{requirementId}/assignments`
|
||||
|
||||
**VO**: `AssignmentClearRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务工作台清空整个住宿需求下全部配房行,调用该接口一次性清空、同步作废应付台账。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `requirementId` | Path | Long | ✅ | — | 房型需求 id |
|
||||
|
||||
#### 出参 `Result<AssignmentClearRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `clearedCount` | Integer | 清空行数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/hotel-requirements/2104045736624820226/assignments
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
清空成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"clearedCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
需求无配房行时 `clearedCount=0`,接口仍 200:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"clearedCount": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
清空范围内任一配房的应付台账行被付款申请占用或已付款,整体拒绝(一条都不删):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 599602,
|
||||
"message": "应付款台账行已锁定",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **整体性**:检查待清空全部 CONFIRMED 行的应付台账,任一被占用则整笔拒绝(失败时零行被删)
|
||||
- **应付行同步作废**:清空成功后同事务作废该需求对应的全部 NORMAL 台账行(无论是否被占用)
|
||||
- **不动抢单**:清空仅删配房,不释放抢单人、不回抢单池
|
||||
- **幂等性**:同需求 3 秒内重复清空拒绝
|
||||
- **部分被占用的场景**:假设需求 2 个 CONFIRMED 行,其中 1 个台账被占用,整批拒、2 行都保留
|
||||
|
||||
### 5. 清空某天配房 `DELETE /v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}`
|
||||
|
||||
**VO**: `AssignmentClearRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
房务工作台行程日行内「清空」按钮,调用该接口清空该天全部配房,无关其他天。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `requirementId` | Path | Long | ✅ | — | 房型需求 id |
|
||||
| `dayNumber` | Path | Integer | ✅ | ≥1 | 第几晚(从 1 开始)|
|
||||
|
||||
#### 出参 `Result<AssignmentClearRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `clearedCount` | Integer | 清空行数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
DELETE /v3/admin/order/hotel-requirements/2104045736624820226/assignments/days/1
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
清空成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"clearedCount": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
该天无配房行时 `clearedCount=0`,接口仍 200:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"clearedCount": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
该天清空范围内任一配房的应付台账行被占用,整体拒绝:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 599602,
|
||||
"message": "应付款台账行已锁定",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **按日分流**:仅清空该天配房,不影响其他晚次
|
||||
- **应付行同步**:清空成功后同事务作废该天对应的应付台账行
|
||||
- **台账冻结**:该天任一台账被占用,整天清空拒绝
|
||||
- **幂等性**:同 (requirementId, dayNumber) 3 秒内重复清空拒绝
|
||||
- **与单行删除互斥**:整天清空与单行删除由同一分布式锁保护
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **团号时点**:收款后(manual-receipt 或 transition 路径产生团号),确认前 team_no 必须非空
|
||||
- **应付台账键**:落库应付行时以 team_no 为业务键,确认失败时台账行零行存在
|
||||
- **改价与标度**:小数点后补零(`265` → `265.00`)不视作改价,应付行不变;需财务的驳回或重新定价时前端提示「联系财务调整」
|
||||
- **台账被占用判定**:`fin_payable_line.applied > 0`(付款申请已占用)或支付单已删除但 `applied` 未清零时视作被占用
|
||||
- **清空原子性**:清空失败为原子操作,检查与删除在同一事务内,失败时零行被删
|
||||
- **权限一致性**:全部接口遵循现有权限校验规则,本单无权限变更
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无 DDL、无迁移、无新增列。行为变化:
|
||||
|
||||
| 表 | 列 | 变化 |
|
||||
|---|---|---|
|
||||
| `house_hotel_assignment` | `confirm_status` | 确认拒绝 808188 时保持 INQUIRING(不翻态) |
|
||||
| `fin_payable_line` | — | 改价/删除/清空时按新规则作废旧行(deleted_at = 当前时刻) |
|
||||
| `fin_payable_line` | `applied` | 作废时 applied 不清零,保留历史记录 |
|
||||
| 其他表 | — | **无变化** |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 确认前团号为空 → 808188,拒绝,配房行保持 INQUIRING,台账零行
|
||||
- 改价无台账行 → 200,只改配房行
|
||||
- 改价有台账行 → 200,作废旧行按新价重推
|
||||
- 改价新价非法 → 599600,与改前一致
|
||||
- 删除无台账行 → 200,正常软删
|
||||
- 删除有台账被占用 → 599602,拒绝
|
||||
- 清空全部被占用 → 599602,整批拒
|
||||
- 清空部分被占用 → 599602,整批拒(一条都不删)
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
本次新增错误码 **808188** `HOUSE_TEAM_NO_MISSING`:「订单团号未生成(收款后生成),暂不能确认配房」。
|
||||
|
||||
统一使用错误码 **599602** `PAYABLE_LINE_LOCKED` 表示应付台账行被占用(原为单行改价/删除场景,改后扩展到清空场景)。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 变更前 | 变更后 |
|
||||
|---|---|---|
|
||||
| 无团号确认 | 200 成功但不推台账 | **808188 拒绝** |
|
||||
| 无台账改价 | 599601 回滚 | **200 成功(只改价)** |
|
||||
| 无台账删除 | 599601 回滚 | **200 成功(正常删除)** |
|
||||
| 有台账改价 | 599601 / 200 异常组合 | **200 成功(作废旧+新推)** |
|
||||
| 清空整需求 | 无台账冻结校验 | **任一被占用则整体拒** |
|
||||
| 清空按天 | 无台账冻结校验 | **该天任一被占用则整天拒** |
|
||||
| 台账占用拒绝码 | 599602(单行)/ 零(清空) | **统一 599602** |
|
||||
| 路径、入参、出参结构 | — | **全部不变** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| 兼容性 | **对前端 100% 透明**。路径、入参、出参结构、HTTP 方法均未变;行为变更仅增加拒绝场景(无团号确认、台账被占用),不破坏现有成功调用 |
|
||||
| 前端 | **无需改动**。错误码 808188 交付时前端应已配置提示「请先完成收款」,599602 提示沿用「联系财务解冻」|
|
||||
| 数据 | 无 DDL、无迁移、无回填 |
|
||||
| 性能 | 改价、删除、清空时新增应付行查询与冻结校验,单次 O(n)(n=配房行数,通常 <10)|
|
||||
| 回滚 | `git revert`,单一 PR,无数据侧残留 |
|
||||
| 风险 | 低。前置检查只能降低風险、不会升高;应付行作废改为同事务内原子操作,消除中间态 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **零影响**:所有接口的路径、HTTP 方法、入参结构
|
||||
- **零影响**:出参结构(均为 Result<Void> 或 Result<AssignmentClearRespVO>)
|
||||
- **零影响**:权限校验、鉴权闸口
|
||||
- **零影响**:库存扣减逻辑(确认失败不改库存,改价/删除不再动库存)
|
||||
- **零影响**:房型快照、房型冻结逻辑
|
||||
- **零影响**:与定制师的交互(改期、驳回等)
|
||||
- **零影响**:订单状态机推进(resource_ready / confirm 等)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
✅ 2026-09-27 于测试环境网关实测,真实鉴权(房务端)。
|
||||
|
||||
- 网关 `https://api.test.1814.love`,分支 `dev-v3`,部署提交 `57199b539`
|
||||
- pre-flight:`ssh -p 2220 root@192.168.100.236 bash /opt/hulalv/scripts/deploy-status.sh` 确认 COMMIT=57199b539 STATE=ok
|
||||
|
||||
| # | 用例 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| AC-1 | 无台账行改价 / 删除 | 200,无台账新增 | ✅ 改价 0→0;删除 0→0(夹具:2 条存量行) |
|
||||
| AC-2 | 确认后删除、清空、改价 | 200,应付行同步作废 | ✅ confirmed→deleted_at(SQL 验证) |
|
||||
| AC-3 | 清空时付款草稿占用 | 599602,0 行被删 | ✅ 整天拒,deleted_at 仍 NULL |
|
||||
| AC-4 | team_no 为空确认 | 808188 拒绝 | ✅ INQUIRING 未翻态,fin_payable_line COUNT=0 |
|
||||
|
||||
**观察**:部署后服务无重启,双实例持续 running,日志最近 200 行 `ERROR` / `Exception` 命中 **0** 条。
|
||||
|
||||
**存量数据**:测试环境所造数据一律 `AC-*` 前缀,验证后已物理删除,残留合计 0 条。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 房务配房 API 规范:`docs/apis/house/assignments/`(dev-v3 分支)
|
||||
- 应付款台账设计:`docs/finance/payable-lines.md`
|
||||
- 团号生成规则:`docs/order/team-no-generation.md`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **Issue**: #8401
|
||||
- **PR**: #8402(合并提交 `57199b539`)
|
||||
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,309 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8410"
|
||||
title: "团期「配置 → 确认」新增只读预检 confirm-check:前端据 ready 提前置灰「确认」按钮,团期级五项与逐户未满足项可见"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "c37fd8ad50d9b94bbae05f1ddf17a793087b4cfa"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "新增 GET /v3/admin/order/group-batch/{groupBatchId}/confirm-check(权限码 group-batch:confirm,与确认写口同码)。返回此刻点「确认」能不能过(ready)、状态是否允许确认(statusConfirmable)、团期级五项(房 / 车 / 导游领队 / 摄影 / 物资,每项 passed + 未通过文案)、逐户未满足项(待支付 / 定制中的在团户,每户 orderNo + items[code, text])与 gateMessage(与此刻点确认拿到的 589556 message 逐字相同)。判据与写口同源;主报账人按「确认时会先补齐人员副本」投影;零写入。状态不是 RESOURCE_PREPARING 时不做逐户预检,checkedHouseholdCount 为 null。前端需在团期详情「配置」节点进入时与点「确认」前调用,据 ready 置灰按钮、据 batchItems / unmetHouseholds 列缺项。【2026-09-27 mmg】前端已交付(c37fd8ad):确认按钮进配置节点+点确认前各调一次 confirm-check,ready=false 置灰+页面级缺项块(五项 batchItems 勾叉+逐户 unmetHouseholds,文案后端直显),非配置节点不调;batch detail spec 14 例+API spec 38 例全绿。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期确认: 新增只读预检 confirm-check,按钮可提前置灰(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8411
|
||||
> **Issue**: #8410
|
||||
> **日期**: 2026-09-27
|
||||
> **影响范围**: 管理后台团期详情「配置」节点的「确认」按钮(置灰与缺项提示);确认写口本身契约不变
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **新增只读端点** `GET .../{groupBatchId}/confirm-check`:点「确认」之前就能知道能不能过、差什么。
|
||||
2. **逐户未满足项第一次对前端可见**。#8339 起确认门含逐户预检(订金、出行人、户级房车、合同模板、主报账人),但团期详情只有团期级五个标记位,前端据此置灰会漏掉逐户这一半;以后以本端点的 `ready` 为准。
|
||||
3. `gateMessage` 与点确认拿到的 589556 `message` 逐字相同,可直接展示。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期「配置 → 确认」(#8268,`POST .../confirm`)的门 = 房 / 车 / 导游领队 / 摄影四项配齐 + 物资已确认,#8339 又加了逐户预检。不满足时整单返回 589556,`message` 里列出未满足项,但**没有结构化数据**,团期详情也只透出团期级五个标记位。于是出现「五项全绿、某户没付订金时按钮是亮的,点下去才报错」。需求确认与订房确认早就各有配对的只读预检(`requirement/confirm-check`、`room-plans/confirm-check`),团期确认补齐同一形态。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期确认预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/confirm-check` | 新增接口 | 只读;与 `POST .../confirm` 同门、同报文、同权限码 |
|
||||
|
||||
网关无改动(在既有 `/v3/admin/order/group-batch` 前缀下)。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期确认预检 `GET /v3/admin/order/group-batch/{groupBatchId}/confirm-check`
|
||||
|
||||
**VO**: `GroupBatchConfirmCheckRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「配置」节点:进入页面时与点「确认」前调用。`ready=false` 时置灰「确认」按钮,用 `batchItems` 展示团期级五项的勾叉,用 `unmetHouseholds` 逐户列出还差什么,或直接展示 `gateMessage`。`statusConfirmable=false`(不在配置节点)时按钮应隐藏或置灰,不要提示缺项。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期主键(雪花 id,按字符串返回) |
|
||||
| batchStatus | String | 团期状态码 |
|
||||
| batchStatusName | String | 团期状态中文名 |
|
||||
| ready | Boolean | 此刻点「确认」能否通过:`statusConfirmable=true` 且五项全过且 `unmetHouseholds` 为空 |
|
||||
| statusConfirmable | Boolean | 状态是否允许确认(仅 `RESOURCE_PREPARING`);false 时点确认会是 589501 |
|
||||
| batchItems | Array | 团期级五项,固定顺序、全量返回(含已通过项),任何状态下都有值 |
|
||||
| batchItems[].code | String | `HOTEL_READY` / `VEHICLE_READY` / `GUIDE_READY` / `PHOTOGRAPHER_READY` / `MATERIAL_CONFIRMED`,与团期详情同名布尔字段对应 |
|
||||
| batchItems[].name | String | 房 / 车 / 导游领队 / 摄影 / 物资 |
|
||||
| batchItems[].passed | Boolean | 是否通过 |
|
||||
| batchItems[].unmetText | String | 未通过时的文案(与 589556 里的逐字相同),通过时 null |
|
||||
| checkedHouseholdCount | Integer | 参与逐户预检的户数(待支付 + 定制中的在团户);`statusConfirmable=false` 时不做逐户预检,为 null(null = 没查,0 = 查了没有需确认的户) |
|
||||
| unmetHouseholds | Array | 不满足的户,按 orderId 升序;全满足或未做逐户预检时为空数组 |
|
||||
| unmetHouseholds[].orderId | String | 子订单 id(字符串) |
|
||||
| unmetHouseholds[].orderNo | String | 子订单号 |
|
||||
| unmetHouseholds[].orderStatus | String | `PENDING_PAY` / `CUSTOMIZING` |
|
||||
| unmetHouseholds[].items | Array | 该户未满足项,顺序同 589556 |
|
||||
| unmetHouseholds[].items[].code | String | 见「六.5、枚举」 |
|
||||
| unmetHouseholds[].items[].text | String | 文案,与 589556 里的逐字相同 |
|
||||
| gateMessage | String | 此刻点确认会拿到的 589556 `message`(逐字相同);`ready=true` 或 `statusConfirmable=false` 时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2097250563497385985/confirm-check HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchStatus": "RESOURCE_PREPARING",
|
||||
"batchStatusName": "资源准备中",
|
||||
"ready": false,
|
||||
"statusConfirmable": true,
|
||||
"batchItems": [
|
||||
{ "code": "HOTEL_READY", "name": "房", "passed": true, "unmetText": null },
|
||||
{ "code": "VEHICLE_READY", "name": "车", "passed": true, "unmetText": null },
|
||||
{ "code": "GUIDE_READY", "name": "导游领队", "passed": true, "unmetText": null },
|
||||
{ "code": "PHOTOGRAPHER_READY", "name": "摄影", "passed": true, "unmetText": null },
|
||||
{ "code": "MATERIAL_CONFIRMED", "name": "物资", "passed": false, "unmetText": "物资未确认" }
|
||||
],
|
||||
"checkedHouseholdCount": 3,
|
||||
"unmetHouseholds": [
|
||||
{
|
||||
"orderId": "2097260000000000001",
|
||||
"orderNo": "GT-26-0085",
|
||||
"orderStatus": "CUSTOMIZING",
|
||||
"items": [
|
||||
{ "code": "PAYMENT_OK", "text": "未付订金" },
|
||||
{ "code": "PRIMARY_REPORTER_MISSING", "text": "未指定主报账人" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"gateMessage": "团期尚不满足确认条件:物资未确认;订单 GT-26-0085:未付订金、未指定主报账人"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
全部满足:`ready=true`,`unmetHouseholds=[]`,`gateMessage=null`。不在配置节点(如招募中、已确认):`statusConfirmable=false`、`ready=false`、`checkedHouseholdCount=null`、`unmetHouseholds=[]`、`gateMessage=null`,`batchItems` 照常返回五项。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"groupBatchId": "2097250563497385985",
|
||||
"batchStatus": "MATERIAL_PREPARING",
|
||||
"batchStatusName": "物料准备中",
|
||||
"ready": false,
|
||||
"statusConfirmable": false,
|
||||
"batchItems": [
|
||||
{ "code": "HOTEL_READY", "name": "房", "passed": true, "unmetText": null },
|
||||
{ "code": "VEHICLE_READY", "name": "车", "passed": true, "unmetText": null },
|
||||
{ "code": "GUIDE_READY", "name": "导游领队", "passed": true, "unmetText": null },
|
||||
{ "code": "PHOTOGRAPHER_READY", "name": "摄影", "passed": true, "unmetText": null },
|
||||
{ "code": "MATERIAL_CONFIRMED", "name": "物资", "passed": true, "unmetText": null }
|
||||
],
|
||||
"checkedHouseholdCount": null,
|
||||
"unmetHouseholds": [],
|
||||
"gateMessage": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不存在:
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
|
||||
```
|
||||
|
||||
无权限(角色未授 `group-batch:confirm`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 权限码 `group-batch:confirm`,与确认写口同码(授 `GROUP_BATCH_MANAGER` / `ADMIN`);看不到「确认」按钮的角色不需要调本端点。
|
||||
- 判据与 `POST .../confirm` 同源:同一阶段判断、同一张五项表、同一套逐户判据、同一个 589556 构造;`gateMessage` 就是那条异常的 message。
|
||||
- 预检只是预检:点确认时写口按当时数据重判一遍,两次调用之间数据变了以写口为准。
|
||||
- 主报账人按「确认时会先补齐团期人员副本」投影:团期层已设主报账人、订单层副本滞后的户,预检不报「未指定主报账人」(点确认时写口会先补齐再判)。
|
||||
- 零写入:不补齐副本、不写时间线、不改状态、不发事件。
|
||||
- 逐户预检的开销与点一次确认相当(每户读一次确认清单),不要轮询调用。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误调用顺序
|
||||
|
||||
| 场景 | 调用 |
|
||||
|------|------|
|
||||
| ✅ 置灰「确认」按钮 | 进入配置节点 → `GET confirm-check` → 按 `ready` 置灰 |
|
||||
| ✅ 点「确认」 | `GET confirm-check`(`ready=true`)→ `POST confirm` |
|
||||
| ❌ 只用详情的五个标记位判断能否确认 | 漏掉逐户预检,点下去仍可能 589556 |
|
||||
| ❌ 自己拼 589556 文案 | 直接用 `gateMessage` 或 `batchItems[].unmetText` / `items[].text` |
|
||||
|
||||
### ready 与 statusConfirmable
|
||||
|
||||
`statusConfirmable=false` 表示「不在能确认的节点」(按钮不该出现或应置灰且不提示缺项);`statusConfirmable=true` 且 `ready=false` 表示「在配置节点但还有缺项」(置灰并展示缺项)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 团期不存在 → 589500;无权限 → 589507(不进业务逻辑)。
|
||||
- 在团户里已是待出行 / 出行中 / 已完成的户不参与逐户预检(与写口一致);已取消户不在「在团」口径内。
|
||||
- 库里若出现认不出的团期状态值,预检返回 `statusConfirmable=false`;确认写口同步改为返回 589501(此前是 500)。正常数据不可达。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### 团期级五项(batchItems[].code)
|
||||
|
||||
**所属字段**: `batchItems[].code` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 未通过文案 |
|
||||
|----|------|------------|
|
||||
| `HOTEL_READY` | 房 | 房未配齐 |
|
||||
| `VEHICLE_READY` | 车 | 车未配齐 |
|
||||
| `GUIDE_READY` | 导游领队 | 导游领队未配齐 |
|
||||
| `PHOTOGRAPHER_READY` | 摄影 | 摄影未配齐 |
|
||||
| `MATERIAL_CONFIRMED` | 物资 | 物资未确认 |
|
||||
|
||||
### 逐户未满足项(unmetHouseholds[].items[].code)
|
||||
|
||||
**所属字段**: `unmetHouseholds[].items[].code` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `PENDING_PAY` | 待支付(须先付订金或取消) | 订单仍是待支付;此时不再重复列 `PAYMENT_OK` |
|
||||
| `PAYMENT_OK` | 未付订金 | 团期口径付订金即可(沿用单户确认清单编码) |
|
||||
| `TRAVELER_COMPLETE` | 出行人信息 | 文案取单户确认清单的失败原因 |
|
||||
| `HOTEL_DONE` | 房型安排 | 同上 |
|
||||
| `VEHICLE_DONE` | 用车安排 | 同上 |
|
||||
| `CONTRACT_TEMPLATE_OK` | 合同方案配置 | 同上 |
|
||||
| `PRIMARY_REPORTER_MISSING` | 未指定主报账人 | 按「确认时先补齐副本」投影后仍无主报账人 |
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 新增一个只读端点。
|
||||
- **零影响**:
|
||||
- `POST .../confirm` 的入参、出参、错误码与门条件(内部改为与预检共用判据;唯一可观察差异是库里出现非法状态值时由 500 改 589501,正常数据不可达)
|
||||
- 团期详情、需求确认预检、订房确认预检
|
||||
- 小程序端
|
||||
- 零数据库变更、零配置变更、零权限种子变更(复用 #8268 的 `group-batch:confirm`)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署:hl-order-service-v3 = dev-v3 @ ecc92b95c(2026-09-27 10:22);取证时 TEST 检出为 dev-v3 @ 57199b539(含 ecc92b95c),经网关 `https://api.test.1814.love` 真实鉴权实测(2026-09-27 10:45–10:56),order-v3 取证期间未被重部署;工单 #8410 已验收关单。
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | ADMIN / GROUP_BATCH_MANAGER / SUPER_ADMIN 调 confirm-check | 200 |
|
||||
| 2 | CUSTOMIZER 调 confirm-check | 589507,前后三表无写入 |
|
||||
| 3 | 不存在的团期 / 不带 token | 589500 / 401 |
|
||||
| 4 | RESOURCE_PREPARING 团期(房 / 车 / 物资未过,12 户定制中) | `batchItems` 五项顺序正确,与详情五个布尔、库值三方一致;`checkedHouseholdCount=12`,12 户逐户列出(TRAVELER_COMPLETE / HOTEL_DONE / VEHICLE_DONE / PRIMARY_REPORTER_MISSING) |
|
||||
| 5 | 同一团期调 `POST .../confirm` 对照 | 589556,`message` 与预检 `gateMessage` 逐字相同(914 字);确认被拒无写入 |
|
||||
| 6 | 团期层临时设主报账人、订单层副本滞后(NONE) | 预检不再报「未指定主报账人」(按确认时先补齐副本投影),预检不补齐副本;还原后恢复原状 |
|
||||
| 7 | 订单层副本是 PRIMARY 但团期层为 NONE(将被补齐降级) | 预检仍报「未指定主报账人」,预检不改该行;还原后恢复原状 |
|
||||
| 8 | 连续四次调预检 | `order_group_batch` / `group_batch_status_log` / `order_staff_assignment` / 子订单状态逐字段无变化 |
|
||||
| 9 | RECRUITING / MATERIAL_PREPARING / CANCELLED 团期 | `statusConfirmable=false`、`ready=false`、`checkedHouseholdCount=null`、`unmetHouseholds=[]`、`gateMessage=null`,五项照返 |
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #8302 | #8268 | 团期人工确认写口(五项门、589556) | ✅ |
|
||||
| #8346 / #8349 | #8339 | 确认门逐户预检、确认前补齐人员副本 | ✅(本端点投影的就是这次补齐) |
|
||||
| — | #7210 | 需求确认预检 `requirement/confirm-check`(同形先例) | ✅ |
|
||||
| **本 PR #8411** | **#8410** | 团期确认只读预检 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8410](https://git.1814.love/wx/HL/issues/8410)
|
||||
- 关联 PR: [wx/HL#8411](https://git.1814.love/wx/HL/pulls/8411)
|
||||
- 写口条目:`24_8268_团期人工确认端点与确认后才出合同保险-新增接口-管理后台.md`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8410](https://git.1814.love/wx/HL/issues/8410)
|
||||
- **PR**: [#8411](https://git.1814.love/wx/HL/pulls/8411)
|
||||
- **Merge commit**: [ecc92b95c](https://git.1814.love/wx/HL/commit/ecc92b95ca2cc9c5801f2cfab5f29456f0419875)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,381 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8413"
|
||||
title: "团期看板导出与统计条补齐列表新筛选:导出接收团期状态 / 截止日 / 出发区间 / 排序(看到什么导什么),统计条接收截止日 / 出发区间;三接口截止日非法值改为忽略"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "31e17e92726aa8d4251e60b107dc2d6ea8fb67d3"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "jw 2026-09-27 定:统计条只跟截止日、出发区间(不跟团期状态 / 看板桶 / 排序);导出看到什么导什么(传排序同列表,不传与列表各自默认一致:按产品 = 出发日升序、不按产品 = 建团时间倒序);导出不带分页、保持 2000 行上限。PR #8417(主体)+ #8432(截止日宽容解析)已合 dev-v3(dada3a8f4)并部署 TEST。2026-09-27 11:54~15:12 经真实网关实测:按产品与全局两种基底、团期状态 / 截止日 / 出发区间组合与三个排序字段 × 升降序、不传排序,导出行集与顺序均与列表已建团行逐一致;产品侧改期(快照 12-10 / 实时 12-15)后按实时日期筛,列表与导出同时命中、按快照日期同时不命中;统计条 total = 列表 total = 七桶之和,filteredOutCount 非零场景(8 / 15)与「ALL − ONGOING」一致;统计条多传团期状态 / 看板桶 / 排序结果不变;非法截止日期三接口均 200 且等同不传该条件(改前列表与导出 500)。TEST 全库团期仅 298 条,2000 行上限由单测覆盖。前端需把列表当前筛选同步传给统计条与导出,故 frontend_status 记 pending。前端已交付:看板 UI 无团期状态/报名截止日筛选控件(grep 实证筛选事实源仅产品/桶/月/关键词/出团区间/排序),batchStatus/deadline 三参无事实源不落;store fetchSummary 补 depart 对(空值不落参,排序/桶仍不带),onExport 补 depart 对+sortBy/sortOrder(看到什么导什么);截止日非法值忽略对前端无影响。store spec 13 例+看板页 spec 10 例全绿,checkpoint 全量通过。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期看板:导出与统计条跟上列表筛选(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8417、#8432
|
||||
> **Issue**: #8413
|
||||
> **日期**: 2026-09-27
|
||||
> **影响范围**: 管理后台「团期订单」看板的统计条与「导出」按钮;列表接口仅截止日非法值的处理变化
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **导出新增入参**:`batchStatus`、`deadlineFrom` / `deadlineTo`、`departFrom` / `departTo`、`sortBy` / `sortOrder`,与列表同名同义。前端把列表**当前的全部筛选和排序**原样传给导出,导出的行和顺序就与列表一致(导出不含未建团行,这一点不变)。
|
||||
2. **导出不传排序时的默认顺序变了(仅不按产品时)**:以前一律按出发日升序;现在与列表一致——**按产品看 = 出发日升序,不按产品看 = 建团时间倒序**。
|
||||
3. **统计条新增入参**:`deadlineFrom` / `deadlineTo`、`departFrom` / `departTo`。**不接收**团期状态、看板桶、排序——统计条是按桶分组的计数,前端传了也会被忽略。
|
||||
4. **报名截止日非法值不再报 500**:列表、导出、统计条传非法截止日期(如 `bad-date`、`2026-02-30`)时忽略该条件,与出发区间一致。以前列表和导出会整页返回 500。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
列表先后加了团期状态、报名截止日、出发区间(#7770)和排序(#7770),导出和统计条没跟上:运营在列表上筛完,顶部统计条数字对不上,导出拿到的也是没筛的全量、顺序也不同。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 导出团期列表 | GET | `/v3/admin/order/group-batch/export` | 修改 | 新增团期状态 / 截止日 / 出发区间 / 排序入参;不按产品时默认顺序改为建团时间倒序 |
|
||||
| 2 | 团期看板统计条 | GET | `/v3/admin/order/group-batch/summary` | 修改 | 新增截止日 / 出发区间入参 |
|
||||
| 3 | 团期分页列表 | GET | `/v3/admin/order/group-batch` | 修改 | 截止日非法值由 500 改为忽略该条件 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 导出团期列表 `GET /v3/admin/order/group-batch/export`
|
||||
|
||||
**VO**: `GroupBatchListReqVO`(入参,与列表同一个)→ CSV 附件(固定 10 列,不变)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板点「导出」,导出当前筛选下的全部已建团团期(不分页,单次上限 2000 行)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productId | query | Long | 否 | — | 原有 |
|
||||
| month | query | String | 否 | yyyy-MM | 原有,决定文件名 |
|
||||
| keyword | query | String | 否 | — | 原有 |
|
||||
| opsStage | query | String | 否 | 七桶 code | 原有 |
|
||||
| scope | query | String | 否 | ONGOING / FINISHED / ALL | 原有 |
|
||||
| batchStatus | query | String | 否 | 九态 code | **新增**,精确匹配 |
|
||||
| deadlineFrom / deadlineTo | query | String | 否 | yyyy-MM-dd,非法值忽略 | **新增**,报名截止日区间 |
|
||||
| departFrom / departTo | query | String | 否 | yyyy-MM-dd,非法值忽略 | **新增**,出团日期区间 |
|
||||
| sortBy | query | String | 否 | departDate / enrollDeadline / createTime,非法值忽略 | **新增** |
|
||||
| sortOrder | query | String | 否 | asc / desc,缺省或非法 asc | **新增** |
|
||||
|
||||
`pageNo` / `pageSize` 即使传了也不生效(导出不分页)。
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| (CSV 附件) | text/csv | 列:团期号、期号、日期、出团日、满团名额、已售、剩余、状态、子订单数、整团应收;不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/export?productId=2056944943066132481&scope=ALL&batchStatus=RECRUITING&deadlineFrom=2026-10-01&deadlineTo=2026-12-31&departFrom=2026-10-01&departTo=2027-01-31&sortBy=departDate&sortOrder=desc
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
成功时直接返回 CSV 文件流(`Content-Disposition: attachment`),不套 Result 信封。
|
||||
|
||||
```json
|
||||
{
|
||||
"说明": "成功响应为 CSV 附件,此处仅示意首行表头",
|
||||
"header": "团期号,期号,日期,出团日,满团名额,已售,剩余,状态,子订单数,整团应收"
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时返回只含表头的 CSV。按产品导出时要调产品服务取实时日期,产品服务不可用返回 589515(与列表、统计条一致):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589515,
|
||||
"message": "获取团期产品列表失败,请稍后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| code | 触发条件 |
|
||||
|---|---|
|
||||
| `589517` | 全部筛选后命中超过 2000 行 |
|
||||
| `589515` | 按产品导出时产品服务不可用 |
|
||||
| `589507` | 无导出权限(`group-batch:export`,未改) |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589517,
|
||||
"message": "导出数据量超过单次上限,请缩小筛选范围后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **看到什么导什么**:同一组参数下,导出行 = 列表里已建团的行,顺序一致(未建团行本来就不导出)。
|
||||
- **不传排序**:按产品看 = 出发日升序;不按产品看 = 建团时间倒序(**以前一律出发日升序**)。
|
||||
- **按产品导出**时,班期范围、截止日、出发区间按**产品侧实时日期**筛选和排序,与列表一致;产品改期后不会再出现「列表里有、导出里没有」。
|
||||
- 2000 行上限在全部筛选之后判断。
|
||||
|
||||
### 2. 团期看板统计条 `GET /v3/admin/order/group-batch/summary`
|
||||
|
||||
**VO**: `GroupBatchSummaryVO`(不变)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板顶部统计条(总数 + 七桶计数 + 被过滤条数),随列表的截止日、出发区间一起刷新。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| productId / month / keyword / scope | query | — | 否 | — | 原有 |
|
||||
| deadlineFrom / deadlineTo | query | String | 否 | yyyy-MM-dd,非法值忽略 | **新增** |
|
||||
| departFrom / departTo | query | String | 否 | yyyy-MM-dd,非法值忽略 | **新增** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| total | Integer | 命中团期数(与列表同条件、不筛团期状态和看板桶时的 total 相等) |
|
||||
| buckets | Map | 七桶计数(之和 = total)+ 旧八桶别名键(#8271 过渡期兼容,不变) |
|
||||
| subOrderCount / minToForm / maxRooms | Integer | 不变 |
|
||||
| effectiveScope | String | 生效的班期范围,不变 |
|
||||
| filteredOutCount | Integer | 因班期范围被过滤掉的条数,**现在带上截止日与出发区间计算** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/summary?productId=2056944943066132481&scope=ONGOING&departFrom=2026-05-01&departTo=2026-12-31
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"total": 22,
|
||||
"buckets": {
|
||||
"RECRUIT": 4, "CONFIGURE": 7, "CONFIRM": 0, "TRIP": 0, "REVIEW": 7, "SETTLE": 1, "DISBANDED": 3,
|
||||
"FORMED": 7, "PENDING_TRIP": 0, "TRAVELLING": 0, "TRIP_FINISHED": 0, "AUDITING": 7, "CHECKED": 1
|
||||
},
|
||||
"subOrderCount": 42,
|
||||
"minToForm": null,
|
||||
"maxRooms": null,
|
||||
"effectiveScope": "ONGOING",
|
||||
"filteredOutCount": 8
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时 `total=0`、各桶为 0:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "total": 0, "buckets": { "RECRUIT": 0 }, "filteredOutCount": 0 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
判权未改(`group-batch:list`)。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 不接收 `batchStatus`、`opsStage`、`sortBy`、`sortOrder`,传了也忽略(统计条要给出全部桶的计数)。
|
||||
- 不传新参数时结果与改前一致。
|
||||
|
||||
### 3. 团期分页列表 `GET /v3/admin/order/group-batch`
|
||||
|
||||
**VO**: `GroupBatchListReqVO` → `PageResult<GroupBatchPageItemRespVO>`(均不变)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
看板列表。本次只改报名截止日非法值的处理。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| deadlineFrom / deadlineTo | query | String | 否 | yyyy-MM-dd | **非法值改为忽略该条件**(以前 500) |
|
||||
| 其余 | query | — | 否 | — | 不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| (全部) | — | 不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch?deadlineFrom=bad-date&pageNo=1&pageSize=20
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 298, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不变。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
判权未改。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 非法截止日期等同不传该条件,服务端记 warn 日志。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 前端把列表当前的筛选参数**同一份**传给统计条和导出:
|
||||
- 导出:`productId / month / keyword / opsStage / scope / batchStatus / deadlineFrom / deadlineTo / departFrom / departTo / sortBy / sortOrder`;
|
||||
- 统计条:`productId / month / keyword / scope / deadlineFrom / deadlineTo / departFrom / departTo`。
|
||||
- 统计条不要传团期状态和看板桶;传了会被忽略,但不要依赖这一点做业务判断。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 零表结构变更、零迁移脚本;三个接口均只读(导出成功后照旧写导出留痕,逻辑未改)。
|
||||
- 三个接口的筛选条件由同一段代码生成,不按产品时下推数据库,按产品时日期类条件按产品侧实时日期在内存筛选。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 导出单次上限 2000 行不变,判断在全部筛选之后。
|
||||
- 按产品导出时,`month` 与 `keyword` 按订单侧快照匹配、CSV「日期 / 出团日」列打印订单侧快照:产品侧改过期或改过名的班期,这两处可能与列表显示不同。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 项 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 导出可用筛选 | productId / month / keyword / opsStage / scope | 另加 batchStatus / 截止日 / 出发区间 / 排序 |
|
||||
| 导出默认顺序(不按产品) | 出发日升序 | 建团时间倒序(与列表一致) |
|
||||
| 导出默认顺序(按产品) | 出发日升序(订单快照) | 出发日升序(产品侧实时) |
|
||||
| 统计条可用筛选 | productId / month / keyword / scope | 另加截止日 / 出发区间 |
|
||||
| 非法截止日期 | 列表、导出 500 | 三接口忽略该条件 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- 前端:统计条与导出要补传参数才生效;不补传时统计条结果不变,导出仅「不按产品」时默认顺序变化。
|
||||
- 列表:只有非法截止日期的处理变化,合法请求不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 导出 CSV 的列与格式、导出留痕、判权码。
|
||||
- 看板列表 `GET /v3/admin/order/group-batch/board`、产品选择器、团期详情。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署 dev-v3 @ `7c69e8227`(主体)与 `dada3a8f4`(截止日宽容解析)后,经真实网关 `https://api.test.1814.love` 实测:
|
||||
|
||||
| 用例 | 期望 | 实测 |
|
||||
|---|---|---|
|
||||
| 构建身份:统计条 departFrom=2099-01-01 | total 由 33 变 0 | 通过 |
|
||||
| 按产品:scope=ALL 不传排序 / 团期状态+截止日+出发区间 / 3 排序字段 × 升降序 | 导出行集与顺序 = 列表已建团行 | 8 组全部一致(列表 34 条含 5 条未建团,导出 29 行) |
|
||||
| 全局:出发区间不传排序 / 团期状态+截止日 / 3 排序字段 × 升降序 | 同上 | 8 组全部一致(169 / 165 / 260 行) |
|
||||
| 产品侧改期(快照 12-10、实时 12-15) | 按实时日期命中、按快照不命中,列表与导出一致 | 出发区间与截止日各两组,通过 |
|
||||
| 统计条 截止日+出发区间(按产品 / 全局,ALL / ONGOING) | total = 列表 total = 七桶之和 | 通过(21 / 23 / 229 / 260) |
|
||||
| 统计条 filteredOutCount 非零场景 | = ALL − ONGOING | 8 / 15,通过 |
|
||||
| 统计条多传团期状态 / 看板桶 / 排序 | 结果不变 | 通过 |
|
||||
| 非法截止日期 `bad-date` / `2026-02-30` | 三接口 200,等同不传 | 通过(改前列表与导出 500) |
|
||||
| 非法出发日期 / 非法排序 | 忽略 | 通过 |
|
||||
| 不带 token | 401 | 通过 |
|
||||
|
||||
TEST 全库团期 298 条,2000 行上限(589517)由单测覆盖。造数已清理(产品侧日期已改回、班期已取消)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #8413、PR #8417、PR #8432
|
||||
- 列表出团区间与排序:Issue #7770
|
||||
- 统计条被过滤条数:Issue #7939
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg
|
||||
@@ -0,0 +1,547 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8418"
|
||||
title: "团期名单 GB-ADM-003 逐户纯增流程 / 核单 / 结算三对状态字段:flowStatus / reviewStatus / settlementStatus 及各自中文名"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "6804cfb99eeebe1bebefc250b3fae9fc14ef587e"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "PR #8420 已合并 dev-v3(58da1df2a),已部署 TEST 并经网关验收 AC-1~AC-9 全部通过。GET /v3/admin/order/group-batch/{groupBatchId}/orders 的 records[] 每户纯增 6 个只读字段:flowStatus / flowStatusName、reviewStatus / reviewStatusName、settlementStatus / settlementStatusName;入参、判权、分页、排序与既有字段全部不变,零 DDL、零写入。前端需:名单「状态」列改用 flowStatusName(原型里的 processStatus 就是它,后端从未提供过 processStatus);需要单独展示核单 / 结算进度时用 reviewStatusName / settlementStatusName。注意 reviewStatus / settlementStatus 与团期核单 GroupSettlementRespVO(#8361)同名字段含义相反,与财务 tab 的 items[].settleStatus 也不是一回事。前端已交付(仅改「状态」列为 flowStatusName 最小接法,用户拍板;核单/结算列未加):RosterTable「状态」列由 orderStatusName 改读 flowStatusName、Name null 回落 flowStatus 原码;api JSDoc 补三对契约+同名不同义警示。spec 6 例全绿,checkpoint 全量通过。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期名单: GB-ADM-003 逐户补流程 / 核单 / 结算三对状态(管理后台)
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8420
|
||||
> **Issue**: #8418
|
||||
> **日期**: 2026-09-27
|
||||
> **影响范围**: 管理后台团期详情「子订单」tab 名单的逐户状态列
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **纯增 6 个响应字段,既有字段一个不动**:`records[]` 每户新增 `flowStatus` / `flowStatusName`(流程细状态)、`reviewStatus` / `reviewStatusName`(逐户核单)、`settlementStatus` / `settlementStatusName`(逐户结算,即财务复核)。
|
||||
2. **同名不同义,别拿错**:本名单的 `reviewStatus` 是**逐户核单**、`settlementStatus` 是**逐户结算**;团期核单接口 `GroupSettlementRespVO`(#8361)里同名的 `reviewStatus` 是**团级复核**、`settlementStatus` 是**团级核单**,含义正好相反,而且那边是一团一行。
|
||||
3. **不是尾款结清**:财务 tab(GB-ADM-040)的 `items[].settleStatus`(已结清 / 待收尾款 / 已退团)表示尾款收没收齐,和本名单的 `settlementStatus` 无关。
|
||||
4. **旧版接口文档里的 `processStatus` 从未实现**,接口文档 GB-ADM-003 已改为 `flowStatus` / `flowStatusName`,按原型做「状态」列请读 `flowStatusName`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期进入核单以后,运营在团期详情「子订单」tab 里看不出每户走到了核单 / 结算的哪一步,只能逐户点进订单详情看。#8340 / #8341 已让子订单的流程、核单、结算三列跟随团期动作同步写入,本单把这三列连同中文名原样透出到名单上。本单**只读透出**,不改任何写入与流转。
|
||||
|
||||
三列由下列动作写入(前端理解状态来源用,本单不改):
|
||||
|
||||
| 动作 | flowStatus | reviewStatus | settlementStatus | 来源 |
|
||||
|---|---|---|---|---|
|
||||
| 团期出行完毕 | `PENDING_REVIEW` 待核单 | `PENDING` 待核算 | 不写 | #8340 |
|
||||
| 团期发起核单 | `REVIEWING` 核单中 | `IN_PROGRESS` 核算中 | 不写 | #8341 |
|
||||
| 逐户核单提交(定稿) | `PENDING_SETTLE` 待结算 | `COMPLETED` 已完成 | `PENDING` 待财务复核 | 既有 |
|
||||
| 团期结算 `/settle` | `SETTLED` 已结算 | `COMPLETED` 已完成 | `COMPLETED` 已结算 | #8341 |
|
||||
| 团期反结算 `/settle/reopen` | `PENDING_SETTLE` 待结算 | `COMPLETED` 已完成 | `PENDING` 待财务复核 | #8341 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期下子订单列表(GB-ADM-003 名单) | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 出参新增字段 | `records[]` 每户纯增 6 个字段,入参与既有字段不变 |
|
||||
|
||||
网关无改动;无新增权限码;无新增错误码;无 DB schema 变更。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期下子订单列表(GB-ADM-003 名单) `GET /v3/admin/order/group-batch/{groupBatchId}/orders`
|
||||
|
||||
**VO**: `PageResult<GroupBatchOrderItemRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台团期详情「子订单」tab 加载名单时调用,一户一行。本次起每行多带流程细状态、逐户核单、逐户结算三对「码 + 中文名」,名单「状态」列与核单 / 结算进度不必再逐户进订单详情查。
|
||||
|
||||
#### 入参
|
||||
|
||||
本单不变。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期须存在 | 团期 ID |
|
||||
| page | Query | Integer | 否 | 缺省 1;<1 归一为 1 | 页码,从 1 起 |
|
||||
| pageSize | Query | Integer | 否 | 缺省 20;<1 归一为 20;>200 截断为 200 | 每页条数 |
|
||||
| includeTravelers | Query | Boolean | 否 | 缺省 true | 是否附出行人明细(证件号 / 手机号一律不返回) |
|
||||
| includeNeeds | Query | Boolean | 否 | 缺省 true | 是否附房数 / 房型 / 特殊需求 |
|
||||
| includeCancelled | Query | Boolean | 否 | 缺省 false | 是否含已取消子订单(缺省只返在团户) |
|
||||
|
||||
#### 出参 `Result<PageResult<GroupBatchOrderItemRespVO>>`
|
||||
|
||||
分页信封:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records | Array\<Object\> | 本页子订单,一户一行(字段见下表) |
|
||||
| total | Integer | 符合条件的子订单总数 |
|
||||
| page | Integer | 当前页码 |
|
||||
| pageSize | Integer | 每页条数 |
|
||||
|
||||
`records[]` 单行(**加粗为本次新增**,其余不变):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | String(Long) | 子订单 ID |
|
||||
| orderNo | String | 订单编号 |
|
||||
| teamNo | String | 团号(订金支付成功后生成;未付订金为 null) |
|
||||
| customerName | String | 客户姓名 |
|
||||
| participantCount | Integer | 出行人总数 |
|
||||
| orderStatus | String | 订单状态码 |
|
||||
| orderStatusName | String | 订单状态中文名 |
|
||||
| **flowStatus** | String | **新增**。流程细状态码,`order_main.flow_status` 原值,与订单详情 `main.flowStatus` 同值;`OrderFlowStatus` 12 值,见六.5 |
|
||||
| **flowStatusName** | String | **新增**。流程细状态中文名,与订单详情 `GET /v3/admin/order/{id}` 的 `main.flowStatusName` 同规则:`AWAITING_PAY` 显示「待补全信息」,其余取枚举中文名;码为 null 时为 null;未知码回落原码 |
|
||||
| **reviewStatus** | String | **新增**。逐户核单状态码,`order_main.review_status`:`NONE` / `PENDING` / `IN_PROGRESS` / `COMPLETED`;出行前为 null |
|
||||
| **reviewStatusName** | String | **新增**。逐户核单状态中文名,与订单详情 `main.reviewStatusName` 同规则:`NONE`、`PENDING` →「待核算」,`IN_PROGRESS` →「核算中」,`COMPLETED` →「已完成」;码为 null 时为 null(不折叠成「待核算」);未知码回落原码 |
|
||||
| **settlementStatus** | String | **新增**。逐户结算状态码(财务复核),`order_main.settlement_status`:`NONE` / `PENDING` / `COMPLETED`,列默认 `NONE` |
|
||||
| **settlementStatusName** | String | **新增**。逐户结算状态中文名:未结算 / 待财务复核 / 已结算;码为 null 时为 null;未知码回落原码 |
|
||||
| payStatus | String | 支付状态 `UNPAID` / `DEPOSIT_PAID` / `FULLY_PAID` |
|
||||
| payStatusName | String | 支付状态中文名 |
|
||||
| contractStatus | String | 合同状态(无合同为 null) |
|
||||
| contractStatusName | String | 合同状态中文名(无合同为 null) |
|
||||
| insuranceStatus | String | 保险状态(无保险为 null) |
|
||||
| insuranceStatusName | String | 保险状态中文名(无保险为 null) |
|
||||
| paidAmount | String(BigDecimal) | 已支付金额 |
|
||||
| balanceAmount | String(BigDecimal) | 待支付尾款金额(≥0) |
|
||||
| hotelRequirementStatus | String | 房需求状态(无有效需求行为 null) |
|
||||
| hotelRequirementStatusName | String | 房需求状态中文名 |
|
||||
| vehicleRequirementStatus | String | 行程用车需求状态(无有效需求行为 null) |
|
||||
| vehicleRequirementStatusName | String | 行程用车需求状态中文名 |
|
||||
| consultantName | String | 定制师姓名 |
|
||||
| totalPrice | String(BigDecimal) | 本户应收(取消单为 `"0.00"`) |
|
||||
| tierCode | String | 档位码,如 `2A1C` |
|
||||
| tierName | String | 档位名,如「2成人1儿童」 |
|
||||
| travelerInfoComplete | Boolean | 出行人资料是否齐全 |
|
||||
| roomCount | Integer | 房数(`includeNeeds=true` 时返回) |
|
||||
| roomType | String | 房型原值(`includeNeeds=true` 时返回) |
|
||||
| roomTypeName | String | 房型中文名 |
|
||||
| specialNeeds | String | 特殊需求(`includeNeeds=true` 时返回) |
|
||||
| contactPhone | String | 联系人手机号(脱敏,前 3 后 4) |
|
||||
| groupChatUnreadCount | Integer | 「联系定制师」团队共享未读数(取不到时为 0) |
|
||||
| travelers | Array\<Object\> | 出行人明细(`includeTravelers=true` 时返回):`name` / `type` / `age` / `birthdayInTrip` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无请求体。
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102000000000000001/orders?page=1&pageSize=20 HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
> 字段结构示意,ID、金额、姓名为示例值。三户分别处于:待结算(核单已提交)、核单中、出行前。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"orderId": "2102000000000000101",
|
||||
"orderNo": "HL20260910100000001",
|
||||
"teamNo": "26-0101",
|
||||
"customerName": "王先生家庭",
|
||||
"participantCount": 3,
|
||||
"orderStatus": "COMPLETED",
|
||||
"orderStatusName": "已完成",
|
||||
"flowStatus": "PENDING_SETTLE",
|
||||
"flowStatusName": "待结算",
|
||||
"reviewStatus": "COMPLETED",
|
||||
"reviewStatusName": "已完成",
|
||||
"settlementStatus": "PENDING",
|
||||
"settlementStatusName": "待财务复核",
|
||||
"payStatus": "FULLY_PAID",
|
||||
"payStatusName": "已付全款",
|
||||
"contractStatus": "SIGNED",
|
||||
"contractStatusName": "已签署",
|
||||
"insuranceStatus": "INSURED",
|
||||
"insuranceStatusName": "已投保",
|
||||
"paidAmount": "12000.00",
|
||||
"balanceAmount": "0.00",
|
||||
"hotelRequirementStatus": "DONE",
|
||||
"hotelRequirementStatusName": "配房完成",
|
||||
"vehicleRequirementStatus": "DONE",
|
||||
"vehicleRequirementStatusName": "配车完成",
|
||||
"consultantName": "张三",
|
||||
"totalPrice": "12000.00",
|
||||
"tierCode": "2A1C",
|
||||
"tierName": "2成人1儿童",
|
||||
"travelerInfoComplete": true,
|
||||
"roomCount": 2,
|
||||
"roomType": "家庭房",
|
||||
"roomTypeName": "家庭房",
|
||||
"specialNeeds": null,
|
||||
"contactPhone": "138****8000",
|
||||
"groupChatUnreadCount": 0,
|
||||
"travelers": [
|
||||
{ "name": "王大明", "type": "ADULT", "age": 38, "birthdayInTrip": false },
|
||||
{ "name": "李小红", "type": "ADULT", "age": 36, "birthdayInTrip": false },
|
||||
{ "name": "王小明", "type": "CHILD", "age": 6, "birthdayInTrip": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"orderId": "2102000000000000102",
|
||||
"orderNo": "HL20260910100000002",
|
||||
"teamNo": "26-0102",
|
||||
"customerName": "刘女士",
|
||||
"participantCount": 2,
|
||||
"orderStatus": "COMPLETED",
|
||||
"orderStatusName": "已完成",
|
||||
"flowStatus": "REVIEWING",
|
||||
"flowStatusName": "核单中",
|
||||
"reviewStatus": "IN_PROGRESS",
|
||||
"reviewStatusName": "核算中",
|
||||
"settlementStatus": "NONE",
|
||||
"settlementStatusName": "未结算",
|
||||
"payStatus": "FULLY_PAID",
|
||||
"payStatusName": "已付全款",
|
||||
"contractStatus": "SIGNED",
|
||||
"contractStatusName": "已签署",
|
||||
"insuranceStatus": "INSURED",
|
||||
"insuranceStatusName": "已投保",
|
||||
"paidAmount": "8000.00",
|
||||
"balanceAmount": "0.00",
|
||||
"hotelRequirementStatus": "DONE",
|
||||
"hotelRequirementStatusName": "配房完成",
|
||||
"vehicleRequirementStatus": "DONE",
|
||||
"vehicleRequirementStatusName": "配车完成",
|
||||
"consultantName": "张三",
|
||||
"totalPrice": "8000.00",
|
||||
"tierCode": "2A",
|
||||
"tierName": "2成人",
|
||||
"travelerInfoComplete": true,
|
||||
"roomCount": 1,
|
||||
"roomType": "大床房",
|
||||
"roomTypeName": "大床房",
|
||||
"specialNeeds": null,
|
||||
"contactPhone": "139****1234",
|
||||
"groupChatUnreadCount": 0,
|
||||
"travelers": [
|
||||
{ "name": "刘芳", "type": "ADULT", "age": 45, "birthdayInTrip": false },
|
||||
{ "name": "陈刚", "type": "ADULT", "age": 47, "birthdayInTrip": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"orderId": "2102000000000000103",
|
||||
"orderNo": "HL20260910100000003",
|
||||
"teamNo": "26-0103",
|
||||
"customerName": "赵先生",
|
||||
"participantCount": 1,
|
||||
"orderStatus": "PENDING_DEPARTURE",
|
||||
"orderStatusName": "待出行",
|
||||
"flowStatus": "PENDING_DEPARTURE",
|
||||
"flowStatusName": "待出行",
|
||||
"reviewStatus": null,
|
||||
"reviewStatusName": null,
|
||||
"settlementStatus": "NONE",
|
||||
"settlementStatusName": "未结算",
|
||||
"payStatus": "DEPOSIT_PAID",
|
||||
"payStatusName": "已付定金",
|
||||
"contractStatus": "SIGNED",
|
||||
"contractStatusName": "已签署",
|
||||
"insuranceStatus": "INSURED",
|
||||
"insuranceStatusName": "已投保",
|
||||
"paidAmount": "1500.00",
|
||||
"balanceAmount": "3500.00",
|
||||
"hotelRequirementStatus": "DONE",
|
||||
"hotelRequirementStatusName": "配房完成",
|
||||
"vehicleRequirementStatus": "DONE",
|
||||
"vehicleRequirementStatusName": "配车完成",
|
||||
"consultantName": "李四",
|
||||
"totalPrice": "5000.00",
|
||||
"tierCode": "1A",
|
||||
"tierName": "1成人",
|
||||
"travelerInfoComplete": true,
|
||||
"roomCount": 1,
|
||||
"roomType": "标间",
|
||||
"roomTypeName": "标间",
|
||||
"specialNeeds": "需要无烟房",
|
||||
"contactPhone": "137****5678",
|
||||
"groupChatUnreadCount": 1,
|
||||
"travelers": [
|
||||
{ "name": "赵磊", "type": "ADULT", "age": 29, "birthdayInTrip": false }
|
||||
]
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团期下没有子订单(或页码超出末页)时 `records` 为空数组,`total` 为真实总数;本接口新增字段无远程调用,无降级分支。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
三列为 null 的户(如出行前的 `reviewStatus`),码与中文名都为 null:
|
||||
|
||||
```json
|
||||
{ "reviewStatus": null, "reviewStatusName": null }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
团期不存在(既有,不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
当前角色未授予 `group-batch:view`,或定制师读取不在本人名下的团期(既有,不变):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判权不变:需 `group-batch:view`;定制师(CUSTOMIZER)另需该团下有本人名下的在团子订单(#7949),否则 589507。判定顺序为权限码 → 定制师归属 → 团期存在(589500)。
|
||||
- 分页、排序、既有字段、`includeTravelers` / `includeNeeds` / `includeCancelled` 语义全部不变;`page` 超出末页返回空 `records`,`total` 仍为真实总数。
|
||||
- 6 个新字段逐户照实读取、只读透出,本接口不写任何状态;`includeCancelled=true` 时已取消户同样照实返回其三列。
|
||||
- `*Name` 规则:码为 null 时中文名也为 null;遇到枚举外的未知码时中文名回落为原码,不报错。
|
||||
- `reviewStatus` 出行前为 null;`settlementStatus` 列默认 `NONE`。
|
||||
- 走 #8361 团核单(finalize / confirm)结算的团,**不回写**这三列,名单照实返回逐户值,可能显示「核算中 / 未结算」;两套口径的收敛归 Epic #8361。
|
||||
- #8341(2026-09-24)之前就已进入核单的存量团没有回填,这些户的三列可能仍为 null / `NONE`。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本接口是只读 GET,入参规则不变;本节只说明新字段该怎么取、别和哪些同名字段混用。
|
||||
|
||||
### ✅ 正确 / ❌ 错误取值对照
|
||||
|
||||
| 场景 | 取值 |
|
||||
|------|------|
|
||||
| ✅ 名单「状态」列(原型里的 `processStatus`) | 读 `flowStatusName`;判定逻辑用 `flowStatus` |
|
||||
| ✅ 单独展示逐户核单进度 | 读 `reviewStatusName`;判定用 `reviewStatus` |
|
||||
| ✅ 单独展示逐户结算(财务复核)进度 | 读 `settlementStatusName`;判定用 `settlementStatus` |
|
||||
| ❌ 按旧版接口文档读 `processStatus` | 该字段从未实现,恒不存在;接口文档 GB-ADM-003 已改为 `flowStatus` / `flowStatusName` |
|
||||
| ❌ 用中文名做判定 | 中文名只用于展示,判定一律用码 |
|
||||
| ❌ 前端自建码 → 中文映射 | 直接显示后端下发的 `*Name`(后端与订单详情同规则) |
|
||||
| ❌ 把 null 渲染成「待核算」 | `reviewStatus` 为 null 表示尚未进入核单,后端不折叠,前端也不要折叠 |
|
||||
|
||||
### 同名 / 近名字段对照(易混点)
|
||||
|
||||
| 出处 | 字段 | 粒度 | 含义 | 取值 |
|
||||
|------|------|------|------|------|
|
||||
| **本名单** GB-ADM-003 `records[]` | `reviewStatus` | 逐户 | 逐户**核单** | `NONE` / `PENDING` / `IN_PROGRESS` / `COMPLETED` |
|
||||
| **本名单** GB-ADM-003 `records[]` | `settlementStatus` | 逐户 | 逐户**结算**(财务复核) | `NONE` / `PENDING` / `COMPLETED` |
|
||||
| **本名单** GB-ADM-003 `records[]` | `flowStatus` | 逐户 | 订单流程细状态 | `OrderFlowStatus` 12 值 |
|
||||
| 团期核单 `GroupSettlementRespVO`(#8361,`GET /v3/admin/order/group-batch/{groupBatchId}/settlement/reports/group` 等) | `reviewStatus` | 团级一行 | 团级**复核** | `PENDING` / `APPROVED` / `RETURNED` |
|
||||
| 团期核单 `GroupSettlementRespVO`(#8361) | `settlementStatus` | 团级一行 | 团级**核单** | `PENDING` / `FINALIZED` / `SETTLED` |
|
||||
| 团期核单 `GroupSettlementRespVO`(#8361) | `flowStatus` | 团级一行 | 团级流程快照 | `TRIP_FINISHED` / `REVIEWING` / `SETTLED` |
|
||||
| 财务 tab GB-ADM-040 `GET /v3/admin/order/group-batch/{groupBatchId}/finance` | `items[].settleStatus` | 逐户 | 尾款是否收齐(实时派生) | 已结清 / 待收尾款 / 已退团 |
|
||||
|
||||
结论:本名单的 `reviewStatus` / `settlementStatus` 与 #8361 同名字段**含义相反**,与 GB-ADM-040 的 `settleStatus` **不是一回事**,三处不可互相替代、不可交叉比对。
|
||||
|
||||
### 两套结算口径并存
|
||||
|
||||
- 走团期结算 `/settle`(#8341)的团:三列随团期动作同步,名单能看到「已结算」。
|
||||
- 走 #8361 团核单 confirm 结算的团:这三列**不回写**,名单照实返回逐户值(可能仍是「核算中 / 未结算」)。两套口径的收敛归 Epic #8361,本单不处理。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
零 DDL、零写入。新字段取自订单主表 `order_main` 既有三列(`flow_status` / `review_status` / `settlement_status`),只读;名单查询本就整行读取,不新增 SQL,也不新增远程调用。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)。
|
||||
- 团期不存在 → 589500;无权限或定制师读非本人名下团期 → 589507(均不变)。
|
||||
- 团期无子订单 → `records: []`、`total: 0`。
|
||||
- 三列为 null 的户 → 码与中文名都为 null。
|
||||
- 未知码 → 中文名回落原码。
|
||||
- 存量团:#8341(2026-09-24)之前已进入核单的团未回填,三列可能为 null / `NONE`。
|
||||
- 走 #8361 团核单 confirm 结算的团:三列不回写,名单照实返回。
|
||||
- 本接口新增字段无远程调用,无降级分支。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### flowStatus(com.hulalv.order.core.enums.OrderFlowStatus)
|
||||
|
||||
**所属字段**: `GroupBatchOrderItemRespVO.flowStatus` / `flowStatusName` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(flowStatusName) | 说明 |
|
||||
|----|------|------|
|
||||
| `AWAITING_PAY` | 待补全信息 | 枚举本名「待支付」,名单与订单详情统一显示「待补全信息」 |
|
||||
| `AWAITING_PROFILE` | 待补全信息 | - |
|
||||
| `RESOURCE_PREPARING` | 资源准备 | - |
|
||||
| `PENDING_CONFIRM` | 待确认 | - |
|
||||
| `PENDING_DEPARTURE` | 待出行 | - |
|
||||
| `TRAVELLING` | 出行中 | - |
|
||||
| `PENDING_REVIEW` | 待核单 | 团期出行完毕时写入(#8340) |
|
||||
| `REVIEWING` | 核单中 | 团期发起核单时写入(#8341) |
|
||||
| `PENDING_SETTLE` | 待结算 | 逐户核单提交后;团期反结算退回此态(#8341) |
|
||||
| `SETTLED` | 已结算 | 团期结算 `/settle` 时写入(#8341) |
|
||||
| `COMPLETED` | 已完成 | - |
|
||||
| `CANCELLED` | 已取消 | - |
|
||||
|
||||
### reviewStatus(com.hulalv.order.settlement.enums.ReviewStatus,逐户核单)
|
||||
|
||||
**所属字段**: `GroupBatchOrderItemRespVO.reviewStatus` / `reviewStatusName` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(reviewStatusName) | 说明 |
|
||||
|----|------|------|
|
||||
| null | null | 出行前未进入核单;不折叠成「待核算」 |
|
||||
| `NONE` | 待核算 | - |
|
||||
| `PENDING` | 待核算 | 团期出行完毕时写入(#8340) |
|
||||
| `IN_PROGRESS` | 核算中 | 团期发起核单时写入(#8341) |
|
||||
| `COMPLETED` | 已完成 | 逐户核单提交后 |
|
||||
|
||||
### settlementStatus(com.hulalv.order.settlement.enums.SettlementStatusEnum,逐户结算 / 财务复核)
|
||||
|
||||
**所属字段**: `GroupBatchOrderItemRespVO.settlementStatus` / `settlementStatusName` | **类型**: `String`
|
||||
|
||||
| 值 | 中文(settlementStatusName) | 说明 |
|
||||
|----|------|------|
|
||||
| `NONE` | 未结算 | 列默认值 |
|
||||
| `PENDING` | 待财务复核 | 逐户核单提交后;团期反结算退回此态(#8341) |
|
||||
| `COMPLETED` | 已结算 | 团期结算 `/settle` 时写入(#8341) |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `records[].flowStatus` | 无此字段 | **新增**:流程细状态码 |
|
||||
| `records[].flowStatusName` | 无此字段 | **新增**:流程细状态中文名(与订单详情同规则) |
|
||||
| `records[].reviewStatus` | 无此字段 | **新增**:逐户核单状态码(出行前 null) |
|
||||
| `records[].reviewStatusName` | 无此字段 | **新增**:逐户核单中文名(与订单详情同规则) |
|
||||
| `records[].settlementStatus` | 无此字段 | **新增**:逐户结算状态码(默认 `NONE`) |
|
||||
| `records[].settlementStatusName` | 无此字段 | **新增**:逐户结算中文名 |
|
||||
| 其余全部字段 / 入参 / 分页信封 | - | 不变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 名单看逐户核单 / 结算进度 | 名单无此信息,需逐户进订单详情 | 名单直接返回三对状态 |
|
||||
| 判权、分页、排序 | - | 不变 |
|
||||
| 状态写入与流转 | - | 不变(本单只读透出) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。只新增响应字段,既有字段的名称、类型、语义不变,旧客户端忽略新字段即可。
|
||||
- **前端是否必须同步上线**: 否。不接入新字段页面照常工作;接入时名单「状态」列用 `flowStatusName`。
|
||||
- **前端 workaround 清理点**: 若名单页曾为展示核单 / 结算进度逐户调订单详情 `GET /v3/admin/order/{id}` 取 `main.flowStatusName`,可改读本名单新字段;若曾按旧版接口文档预留 `processStatus`,改为 `flowStatus` / `flowStatusName`。
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: `GET /v3/admin/order/group-batch/{groupBatchId}/orders` 响应 `records[]` 新增 6 个字段。
|
||||
- **零影响**:
|
||||
- 本接口的入参、判权、分页、排序与其余字段
|
||||
- 团期文档导出与打印(只取订单 ID / 订单号 / 人数 / 房数,不受影响)
|
||||
- 订单详情 `GET /v3/admin/order/{id}` 与订单列表
|
||||
- #8361 团核单各接口(finalize / confirm / `settlement/reports/group`)
|
||||
- 财务 tab GB-ADM-040 `GET /v3/admin/order/group-batch/{groupBatchId}/finance`
|
||||
- 团期核单、结算、反结算的写入与流转(#8340 / #8341 行为不变)
|
||||
- 小程序端
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
TEST 已部署 dev-v3@`58da1df2a`(order-v3 双实例 8086/8186 滚动完成,2026-09-27 12:30)。构建身份探针:连打 8 次名单接口,8 次都带 `flowStatus`。经真实网关 `https://api.test.1814.love` 取证,库为 `hl_order_service_v3` 只读查询。
|
||||
|
||||
| AC | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| AC-1 | 每户返回三对新字段,改前改后响应 diff 只多出这 6 个键 | ✅ 5 个团 12 户部署前后各取一次:逐户「改后键 − 改前键」恰为这 6 个;改前的键一个不少,取值逐键比对 0 差异;户集合、顺序、`total` / `page` / `pageSize` 都不变 |
|
||||
| AC-2 | 五种组合各取至少 1 户,三个码与库一致 | ✅ SETTLED/COMPLETED/COMPLETED 4 户、PENDING_SETTLE/COMPLETED/PENDING 3 户、REVIEWING/IN_PROGRESS/NONE 2 户、PENDING_REVIEW/PENDING/NONE 1 户、PENDING_DEPARTURE/null/NONE 2 户:名单三码与 `order_main` 同一分钟内的读数逐户一致 |
|
||||
| AC-3 | 同一订单名单 `flowStatusName` / `reviewStatusName` 与订单详情 `main.*` 逐字一致 | ✅ 五种组合各 1 户对照 `GET /v3/admin/order/{id}` 的 `data.main`:待出行/null、待核单/待核算、核单中/核算中、待结算/已完成、已结算/已完成,逐字一致(详情不含 settlementStatusName,不在对照范围) |
|
||||
| AC-4 | `reviewStatus` 为 null 的户码与中文名都为 null,不折叠成「待核算」 | ✅ 团 2100509904627191810 两户原始响应为 `"reviewStatus":null,"reviewStatusName":null`,库里 `review_status` 为 NULL |
|
||||
| AC-5 | 未知码中文名回落原码 | ✅ 单测:合并提交上 `GroupBatchConverterTest` 的 #8418 用例 48 条执行、0 失败,含三个中文名各自的 `ZZZ_UNKNOWN` 行,以及 `offEnumCodes_sameAsOrderDetail` 5 条(空串 / 空白 / 未知码 / 小写两例) |
|
||||
| AC-6 | 逐户反确认 → 逐户定稿,每步名单与库一致,最终复原 | ✅ 子订单 2102091018323296257:<br>• 改前 PENDING_SETTLE / COMPLETED / PENDING;<br>• `…/settlement/final-snapshots/reopen` 后名单与库均为 REVIEWING(核单中)/ IN_PROGRESS(核算中)/ NONE(未结算);<br>• `…/settlement/finalize` 后均回到 PENDING_SETTLE(待结算)/ COMPLETED(已完成)/ PENDING(待财务复核);<br>• 快照 v1 → REOPENED,新增 v2 FINALIZED 为当前版本,核单汇总金额与改前一致 |
|
||||
| AC-7 | 名单无新增 SQL / 远程调用,生产代码只改 VO 与转换器 | ✅ 本单生产代码只有 `GroupBatchOrderItemRespVO`、`GroupBatchConverter` 两个文件;`GroupBatchQueryService` / Mapper / `OrderInfoConverter` 零改动 |
|
||||
| AC-8 | order-v3 相关测试不引入新失败 | ✅ 定向 278 条 0 失败(合并提交上复测同样 278/0),16 个 Arch 类 87 条 0 失败;groupbatch 整包 4 个红类在干净基底 `7c69e8227` 上逐类读数吻合,属于既有问题 |
|
||||
| AC-9 | 接口文档 GB-ADM-003 与 API-SPEC §17.3 已更新,changelog 通过校验器 | ✅ 团期接口文档 v2.0、一期实施拆分详设、实施单 02、API-SPEC §17.3、order-v3 CHANGELOG v6.3.30 已随 PR #8420 合入;本条目通过 frontmatter / 文件名校验器 |
|
||||
|
||||
反例:不带 Authorization 或签名被篡改的 token,网关返回 HTTP 200 + `code: 401`(「缺少有效的 Authorization 头」/「Token 无效」),与网关既有约定一致,本单未改判权。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| #8344 | #8340 | 团期单子订单出发 / 返团跟随团期推进(出行完毕写 `PENDING_REVIEW` / `PENDING`) | ✅ 有效,本单只读其结果 |
|
||||
| #8345 | #8341 | 团期核单 / 结算 / 反结算同步子订单三列 | ✅ 有效,本单只读其结果 |
|
||||
| — | #7949 | 定制师读团期名单的归属校验 | ✅ 有效,判权不变 |
|
||||
| — | #8361 | 团期报销核单一团一张(`GroupSettlementRespVO` 团级同名字段) | ✅ 有效,两套口径收敛归该 Epic |
|
||||
| **本 PR #8420** | **#8418** | 名单逐户补三对状态 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8418](https://git.1814.love/wx/HL/issues/8418)
|
||||
- 关联 PR: [wx/HL#8420](https://git.1814.love/wx/HL/pulls/8420)
|
||||
- 接口文档:HL `docs/group/团期模块接口文档-v2.0.html` GB-ADM-003(`processStatus` 已改为 `flowStatus` / `flowStatusName`)
|
||||
- API 规格:HL `docs/order-v3/api/API-SPEC.html` §17.3 `GroupBatchOrderItemRespVO`
|
||||
- order-v3 变更记录:HL `docs/order-v3/CHANGELOG.md` v6.3.30
|
||||
- 数据源:#8340、#8341;口径 Epic:#8361
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8418](https://git.1814.love/wx/HL/issues/8418)
|
||||
- **PR**: [#8420](https://git.1814.love/wx/HL/pulls/8420)
|
||||
- **Merge commit**: [58da1df2a](https://git.1814.love/wx/HL/commit/58da1df2ac781414c104b7c49120afab5287195f)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,770 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8429"
|
||||
title: "车务最终实派方案发布判据统一(批量派车 batch 补上满派与陈旧定稿守卫,已完结行越窗豁免)、三个写口新增未发布原因字段、602202 按声明车数放宽"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "72c28d07a0defe478cce3b97b9ffaf33e8137bc7"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-28"
|
||||
status_note: "PR #8451 已合入 dev-v3(合并提交 267ab6906)。TEST 环境 hl-fleet-service 267ab6906 于 2026-09-27 19:40:15 部署,hl-gateway 71def6dc5 同期在线;2026-09-27 19:58-20:27 在该组合上网关实测 batch/pd/confirm 三写口(见「八、测试环境已验证」):越窗已完结行 batch 与 pd 答案从一真一假变为都真;602202 改按声明车数动态判定(count=2 时第 3 辆拒绝、降为 count=1 后第 2 辆即拒);三个接口响应均带 finalPlanNotPublishedReason 字段。前端已交付(72c28d07):batch 失败按 finalPlanNotPublishedReason 精确指引(仅无原因/GATE_UNSATISFIED 跳第③步,方案不完整/代际/载客量留第②步),confirm 多组 HTTP 成功补查 finalPlanPublished=false 改警示而非「已完成派单」;useAssignFlow 两 spec 84 例全绿。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 车务最终实派方案发布判据统一 + 未发布原因字段
|
||||
|
||||
**服务**: hl-fleet-service(端口 8161/8261)
|
||||
**PR**: `#8451`(已合入 `dev-v3`,合并提交 `267ab6906`)
|
||||
**Issue**: #8429
|
||||
**日期**: 2026-09-27
|
||||
**影响范围**: 管理后台车务看板的批量派车、接送机配置、需求确认三个接口;接送机派车对同日同方向多辆车的上限校验
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **三个写口统一判据。** 改前:batch 只看接送机门禁(满足即发布),pd 与 confirm 还要过满派拓扑与陈旧定稿两道守卫。改后:三个接口都走完整判据(陈旧定稿 → 方案代际 → 满派拓扑 → 门禁)。结果:同一份派单数据走 batch 和 pd 可能得到不同答案的情况消失;batch 在「方案不完整」时会从原来的 `finalPlanPublished=true` 变成 `false` + `PLAN_INCOMPLETE`。
|
||||
|
||||
🟡 **新字段 `finalPlanNotPublishedReason`。** 三个接口(batch、pd、confirm)响应都新增这个字段。发布时为 null;不发布时回「第一个没通过的判据」原因,供前端对运营给出有针对性的提示。取值共五个(通用)+ 两个(pd 专属)。
|
||||
|
||||
🟡 **已完结行越窗豁免。** 改大交通或换版后,已完结日落在新服务日之外。改前会让方案判「不完整」永久发不出去;改后只对在途行检查窗口,已完结行豁免。
|
||||
|
||||
🟢 **602202 按声明车数放宽。** TRANSFER 需求下同日同方向标「参与」的派车行数上限从「1」改为「声明车数」(count)。当 count 缺失或 ≤0 时按 1。code 不变,文案扩展(含日期、方向、标记条数、派车行 ID 列表、声明车数)。TRAVEL 需求不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
**问题现象**(测试服 2026-09-27 实测):同一批派单数据,pd 配齐接送机后响应 `finalPlanPublished=false`,同一条 batch(无新增行)7 秒后响应 `true`。根因是三个写口走不同判据——batch 只过门禁漏斗,pd 还要过满派守卫,当方案含「越窗已完结行」时满派判定会卡住。
|
||||
|
||||
**两个上游缺口**:
|
||||
1. #5810 在「最终确认基线」与「方案代际」两处给已完结行豁免,满派拓扑这一处漏了;
|
||||
2. #7067 统一了发布入口但 batch 路径返工时没有带上满派与陈旧定稿的守卫。
|
||||
|
||||
**业务定案**(wx 2026-09-27):
|
||||
- TRANSFER 的 count = 同日同方向实际要派的车数。
|
||||
- count 不是发布门禁;实派即权威,派少照样发。
|
||||
- 602202 以 count 为上限,不再「超过 1 就拒」。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 批量派车 | POST | `/admin/fleet/assignments/batch` | 修改 | 判据改完整(新增满派与陈旧定稿检查);新增 finalPlanNotPublishedReason;已完结行越窗豁免 |
|
||||
| 2 | 接送机配置 | PUT | `/admin/fleet/assignments/pickup-dropoff-config` | 修改 | 新增 finalPlanNotPublishedReason;602202 上限按声明车数;已完结行越窗豁免 |
|
||||
| 3 | 需求确认 | POST | `/admin/fleet/assignments/requirements/{requirementId}/confirm` | 修改 | 新增 finalPlanNotPublishedReason;已完结行越窗豁免 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 批量派车 `POST /admin/fleet/assignments/batch`
|
||||
|
||||
**VO**: `BatchCreateAssignmentReqVO` → `BatchAssignmentWriteRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务第②步:提交逐日最终方案。**改后走完整发布判据,方案不完整时会从原来的 true 变成 false。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | Long | ✅ | 订单 ID(雪花) | **不变** |
|
||||
| orderNo | Body | String | ❌ | - | **不变**:订单号冗余 |
|
||||
| requirementId | Body | Long | ✅ | 用车需求 ID | **不变**:当前生效用车需求 ID |
|
||||
| kind | Body | String | ❌ | `TRANSFER` / `TRAVEL` | **不变,与本单无关**:不传按 `TRAVEL` 解析(见下方业务边界「kind 省略」) |
|
||||
| startDate | Body | String | ✅ | yyyy-MM-dd | **不变**(此前遗漏未列):用车开始日期;TRANSFER 请传接送机服务日最早一天 |
|
||||
| endDate | Body | String | ✅ | yyyy-MM-dd | **不变**(此前遗漏未列):用车结束日期 |
|
||||
| pickupAt | Body | String | ❌ | - | **不变**:接客地 |
|
||||
| dropoffAt | Body | String | ❌ | - | **不变**:送客地 |
|
||||
| headcount | Body | Integer | ❌ | - | **不变**:乘客人数 |
|
||||
| confirmNoVehicleServiceDates | Body | Boolean | ❌ | - | **不变**:逐日计划未覆盖全部服务日(存在不配车日)时的显式二次确认 |
|
||||
| sendItinerarySms | Body | Boolean | ❌ | 不传按 false | **不变**:是否向本批各车师傅发送行程短信,整批统一决策 |
|
||||
| skipCityJunctionException | Body | Boolean | ❌ | - | **不变**:跳过城市衔接例外 |
|
||||
| fromEntry | Body | String | ❌ | - | **不变**:操作来源 |
|
||||
| requestId | Body | String | ✅ | 长度 ≤64 | **改**:此前误写为非必填,源码 `@NotBlank`,批次级幂等请求标识不能为空 |
|
||||
| dailyPlan | Body | List<DailyPlanItem> | ✅ | 最多 4000 项 | **不变**:按行程日的完整配车列表,需求日期窗内未出现的服务日视为该日不配车 |
|
||||
| dailyPlan[].serviceDate | Body | String | ✅ | yyyy-MM-dd | **不变** |
|
||||
| dailyPlan[].vehicleId | Body | Long | ✅ | 车辆 ID | **不变** |
|
||||
| dailyPlan[].driverId | Body | Long | ✅ | 司机 ID | **不变** |
|
||||
| dailyPlan[].assignmentPrice | Body | BigDecimal | ❌ | ≥0.00,整数位≤10 小数位≤2 | **不变**:本车当天实际价格,未传按车型价格日历参考价兜底 |
|
||||
| dailyPlan[].priceAdjustmentReason | Body | String | ❌ | 长度 ≤256 | **不变**:实际价格与参考价不一致时的调整原因 |
|
||||
| dailyPlan[].confirmCrossResident | Body | Boolean | ❌ | - | **不变**:跨常驻车显式确认 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| assignments | List<Item> | **不变**:本次提交生效的派单结果;`Item.fleetItemIndex` 去槽位化(#7067)后恒为 `null`(字段保留兼容),`Item.assignment` 是完整 `AssignmentWriteRespVO`(见响应示例,关键字段 `id`【不是 `assignmentId`】、`assignmentStatus`【提交即派定,恒 `"assigned"` 小写】) |
|
||||
| finalPlanPublished | Boolean | **语义不变,取值可能改变**:true = 本次发布了最终方案;false 时见下一字段 |
|
||||
| finalPlanNotPublishedReason | String | **新增**。发布时为 null;不发布时取值见下表。返回第一个没通过的判据原因 |
|
||||
| pickupDropoffGate | PickupDropoffGateVO | **不变**:含 `arrivalRequiredDates`/`departureRequiredDates`/`missingPickupDates`/`missingDropoffDates`/`declared`/`satisfied`(见响应示例) |
|
||||
| failedFleetItemIndex | Integer | **不变** |
|
||||
| dailyDifferences | List<...> | **不变**:成功时为 `null`(不是空数组),仅最终派定失败时回逐日基线差异 |
|
||||
|
||||
**finalPlanNotPublishedReason 取值**(五个值,按判据顺序,只返回首个未通过的):
|
||||
|
||||
| 取值 | 触发条件 |
|
||||
|------|----------|
|
||||
| `STALE_FINALIZED_PLAN` | 存在按旧需求定稿的陈旧行,需车务对当前需求重新确认 |
|
||||
| `INVALID_PLAN_GENERATION` | 方案代际不一致(部分行已定稿、部分未定稿或代际不同) |
|
||||
| `PLAN_INCOMPLETE` | 满派拓扑不完整:有逻辑 key 没派车、缺司机、在途行越窗、同 key 多行等 |
|
||||
| `CAPACITY_INSUFFICIENT` | 未定稿分支当日载客量不足覆盖需求人数(仅未定稿分支出现) |
|
||||
| `GATE_UNSATISFIED` | 大交通要求的接/送机日没有配车 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
实测请求(2026-09-27 20:00:24,测试专用车务账号,见 `wf/8429/ac2/result.md` AC-2 第一步):
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": 2104161383220457474,
|
||||
"requirementId": 2104178895018459138,
|
||||
"kind": "TRANSFER",
|
||||
"startDate": "2027-08-15",
|
||||
"endDate": "2027-08-30",
|
||||
"headcount": 2,
|
||||
"requestId": "fleetqa-ac2_step1_batch-xxxxxxxxxx",
|
||||
"dailyPlan": [
|
||||
{
|
||||
"serviceDate": "2027-08-15",
|
||||
"vehicleId": 2104029270659780610,
|
||||
"driverId": 2104029157140910081,
|
||||
"assignmentPrice": "300.00",
|
||||
"priceAdjustmentReason": "按订单约定价格"
|
||||
},
|
||||
{
|
||||
"serviceDate": "2027-08-30",
|
||||
"vehicleId": 2104029270659780610,
|
||||
"driverId": 2104029157140910081,
|
||||
"assignmentPrice": "300.00",
|
||||
"priceAdjustmentReason": "按订单约定价格"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
提交新行、门禁未配齐(不发布)——实测原文(`wf/8429/ac2/out/batch_ac4_assign3.json`,AC-4 三车同时新建):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"assignments": [
|
||||
{
|
||||
"fleetItemIndex": null,
|
||||
"assignment": {
|
||||
"id": "2104183651266863105",
|
||||
"assignmentGroupId": "362573467578667009",
|
||||
"assignmentSlotId": "362573467578667009",
|
||||
"assignmentStatus": "assigned",
|
||||
"stageCode": "assigned",
|
||||
"stageLabel": "已派车",
|
||||
"currentStep": 3,
|
||||
"skippedStepCodes": [],
|
||||
"protocolPrice": "300.00",
|
||||
"vehicleFeeAutoTotal": "0.00",
|
||||
"vehicleFeeAutoComplete": false,
|
||||
"vehicleFeeTotal": "300.00",
|
||||
"vehicleFeeSource": "MANUAL",
|
||||
"vehicleFeeAdjustmentReason": "按订单约定价格",
|
||||
"dailyVehicleFees": null,
|
||||
"holdSentAt": null,
|
||||
"confirmedAt": "2026-09-27 20:17:31",
|
||||
"sideEffects": {
|
||||
"vehicleStatusUpdated": "busy",
|
||||
"driverStatusUpdated": "busy",
|
||||
"reconPrepRowsCreated": 0,
|
||||
"reconPrepMarkedCanceled": null
|
||||
},
|
||||
"sendItinerarySms": false,
|
||||
"itinerarySmsEventId": null,
|
||||
"itinerarySmsStatus": "NOT_SENT",
|
||||
"dailyDifferences": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"finalPlanPublished": false,
|
||||
"finalPlanNotPublishedReason": "GATE_UNSATISFIED",
|
||||
"pickupDropoffGate": {
|
||||
"arrivalRequiredDates": ["2027-08-30", "2027-09-05"],
|
||||
"departureRequiredDates": ["2027-08-09"],
|
||||
"missingPickupDates": ["2027-08-30", "2027-09-05"],
|
||||
"missingDropoffDates": [],
|
||||
"declared": true,
|
||||
"satisfied": false
|
||||
},
|
||||
"failedFleetItemIndex": null,
|
||||
"dailyDifferences": null
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 上例响应只返回 1 条 `assignments` 记录,即便当次提交/相关联的行更多——`assignments[]` 只包含本次调用**新建**的派车行(与库内既有行 diff 后的 `itemsToCreate`),已存在且未变化的行不会重复出现。**不要用 `assignments` 数组长度判断本次提交是否被完整接受**,应以 `finalPlanPublished` / `finalPlanNotPublishedReason` 为准。
|
||||
|
||||
原样重放、已发布(本次未产生新建行)——实测原文(`wf/8429/ac2/out/batch_ac2_step3_batch_replay.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"assignments": [],
|
||||
"finalPlanPublished": true,
|
||||
"finalPlanNotPublishedReason": null,
|
||||
"pickupDropoffGate": {
|
||||
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
|
||||
"departureRequiredDates": ["2027-08-09"],
|
||||
"missingPickupDates": [],
|
||||
"missingDropoffDates": [],
|
||||
"declared": true,
|
||||
"satisfied": true
|
||||
},
|
||||
"failedFleetItemIndex": null,
|
||||
"dailyDifferences": null
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
派单成功但方案不完整时(缺接/送机日、缺司机等)返回 `finalPlanPublished=false` + 原因(无空态)。
|
||||
|
||||
网关降级无额外变化。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605062,
|
||||
"message": "派车日期不在用车需求服务范围内",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 605062 | 提交项的服务日期越出当前需求日期窗(**不变**);既有的窗外在途行不再报错,仅新提交项受限 |
|
||||
| 605912 | 派车快照缺少需求车型:所选车辆无车型大类且型号名无法归一(**不变**) |
|
||||
| 605905 | 需求版本过期:锁内重读的当前生效需求 ID/版本与请求不一致(**不变**);`kind` 省略解析出的需求与传入 `requirementId` 不一致时也走这条 |
|
||||
| 100001 | 参数非法,含「已完结日改派」(该日已完结不可改派)与请求携带已移除旧字段两类场景(**不变**) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **满派拓扑缺口场景**:需求下某些必须配车的服务日本次未提交任何 `dailyPlan` 行覆盖(而非"某行漏填司机"——`dailyPlan[].driverId` 本身必填,无法提交出一条缺司机的行),门禁满足时改前发布、改后不发布(`PLAN_INCOMPLETE`)。运营需按原因提示补齐缺失服务日的派车行。
|
||||
- **已完结行**:越窗已完结行不再卡发布,该行在其他维度(车、司机、形态)仍需满足完整性条件。
|
||||
- **空方案**(dailyPlan 为空):只过门禁判定,满派守卫不适用。
|
||||
- **重复调用**(requestId 相同):按幂等复用,返回相同结果。
|
||||
- **`assignments` 只回新建行**:数组只包含本次调用新建的派车行,已存在且未变化的行不重复返回;判断提交是否被接受用 `finalPlanPublished`/`finalPlanNotPublishedReason`,不要用数组长度或内容对比。
|
||||
- **旧字段一律 400**:`items`/`chargeableServiceDates`/`vehicleFeeWaiverReason`/`confirmAllServiceDatesFree`/`holdMode`(顶层)、`dailyPlan[].fleetItemIndex`/`used`/`pickupParticipant`(逐日项)已移除,请求体携带任一个即触发 Bean Validation 400(不进入业务判据)。
|
||||
- **该接口重置本次覆盖服务日的接送标记**:`batch` 落库时会把本次 `dailyPlan` 覆盖到的每个服务日的 pickup/dropoff 参与标记重置,即使该日车辆/司机未变——先调 `batch` 再调 `pickup-dropoff-config` 的调用顺序下,`batch` 覆盖到的日期需要重新配置接送标记,不能假设旧标记还在。
|
||||
- **`kind` 省略时的解析行为**(`#8453`,纯 TRANSFER 订单既有缺陷,非本次改动引入):`kind` 不是必填字段,省略时按 `TRAVEL` 解析当前需求(`RequirementKindResolver.resolveForMutation`,`AssignmentService.java:8088`),与「接送机配置」接口共用同一个解析器(`AssignmentService.java:1228`)。纯 TRANSFER 订单(无 TRAVEL 需求)省略 `kind` 时会解析不到当前需求;`requirementId` 传了值但与解析结果不一致会触发 605905(需求版本过期)。该缺陷已在「接送机配置」接口实测复现(见该接口业务边界的日志对照),本接口调用同一个解析器,结构上同样受影响,但本轮未针对 `batch` 单独造数复现——TRANSFER 场景请始终显式传 `kind: "TRANSFER"`,跟踪见 `#8453`。
|
||||
|
||||
---
|
||||
|
||||
### 2. 接送机配置 `PUT /admin/fleet/assignments/pickup-dropoff-config`
|
||||
|
||||
**VO**: `PickupDropoffConfigReqVO` → `PickupDropoffConfigRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务第③步:配置派车行是否参与接机/送机。改后新增未发布原因字段,602202 上限按声明车数。**本接口仍保留「跃迁才发布」规则(门禁从不满足→满足时发布),且已完结行越窗不再阻止发布。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Body | Long | ✅ | 订单 ID | **不变** |
|
||||
| requirementId | Body | Long | ✅ | 用车需求 ID | **不变**:当前生效用车需求 ID |
|
||||
| kind | Body | String | ❌ | `TRANSFER` / `TRAVEL` | **改,与本单无关**:不传按 `TRAVEL` 解析(见下方业务边界「kind 省略」,纯 TRANSFER 订单请务必显式传) |
|
||||
| requestId | Body | String | ✅ | 长度 ≤64 | **改**:此前误写为非必填,源码 `@NotBlank` |
|
||||
| items | Body | List<ConfigItem> | ✅ | 最多 4000 项 | **不变**:要标记接送机参与的派车行集合;该订单+当前需求下未出现在 `items` 中的生效派车行两个方向标志一律归零(全量覆盖语义) |
|
||||
| items[].assignmentId | Body | Long | ✅ | 须是当前需求下的生效派车行 | **不变** |
|
||||
| items[].pickupParticipant | Body | Boolean | ✅ | - | **不变**:当日该车是否参与 ARRIVAL 接机 |
|
||||
| items[].dropoffParticipant | Body | Boolean | ✅ | 与 pickupParticipant 不能同为 false | **不变**:当日该车是否参与 DEPARTURE 送机;同一行两个标志不能同为 false(不参与的行不要出现在 `items` 里,否则 400) |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| finalPlanPublished | Boolean | **不变**:是否已发布最终方案 |
|
||||
| finalPlanNotPublishedReason | String | **新增**。七个值(五个通用 + 两个本接口专属),发布时为 null |
|
||||
| requirementReopened | Boolean | **不变**:是否已把订单拉回处理中 |
|
||||
| reopenBlockedReason | String | **不变**:拉回失败的原因 |
|
||||
| pickupDropoffGate | PickupDropoffGateVO | **不变**:当前门禁状态 |
|
||||
|
||||
**finalPlanNotPublishedReason 取值**(七个值):
|
||||
|
||||
| 取值 | 出现场景 |
|
||||
|------|---------|
|
||||
| 前五个 | 同 batch(陈旧定稿、代际不一致、拓扑不完整、载客量不足、门禁不满足) |
|
||||
| `NO_GATE_TRANSITION` | **本接口专属**:本次配置前后门禁没有从不满足→满足 |
|
||||
| `PICKUP_DROPOFF_GATE_DISABLED` | **本接口专属**:接送机门禁开关关闭或无当前生效需求,本接口退回纯写口 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
实测请求(2026-09-27 20:02:18,测试专用车务账号,见 `wf/8429/ac2/result.md` AC-2 第二步、一次性提交两行):
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": 2104161383220457474,
|
||||
"requirementId": 2104178895018459138,
|
||||
"kind": "TRANSFER",
|
||||
"requestId": "fleetqa-pd-ac2_step2_pd-xxxxxxxxxx",
|
||||
"items": [
|
||||
{
|
||||
"assignmentId": 2104166776231403521,
|
||||
"pickupParticipant": true,
|
||||
"dropoffParticipant": false
|
||||
},
|
||||
{
|
||||
"assignmentId": 2104179344782008321,
|
||||
"pickupParticipant": true,
|
||||
"dropoffParticipant": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
门禁尚未满足(部分补标,仍缺 08-15)——实测原文(`wf/8429/ac2/out/pd_ac2_step2_pd.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"finalPlanPublished": false,
|
||||
"finalPlanNotPublishedReason": "NO_GATE_TRANSITION",
|
||||
"requirementReopened": false,
|
||||
"reopenBlockedReason": null,
|
||||
"pickupDropoffGate": {
|
||||
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
|
||||
"departureRequiredDates": ["2027-08-09"],
|
||||
"missingPickupDates": ["2027-08-15"],
|
||||
"missingDropoffDates": [],
|
||||
"declared": true,
|
||||
"satisfied": false
|
||||
}
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
门禁已满足但无新跃迁(重复提交与已生效配置相同的标记)——实测原文(`wf/8429/ac2/out/pd_ac4_reset_v2.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"finalPlanPublished": false,
|
||||
"finalPlanNotPublishedReason": "NO_GATE_TRANSITION",
|
||||
"requirementReopened": false,
|
||||
"reopenBlockedReason": null,
|
||||
"pickupDropoffGate": {
|
||||
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
|
||||
"departureRequiredDates": ["2027-08-09"],
|
||||
"missingPickupDates": [],
|
||||
"missingDropoffDates": [],
|
||||
"declared": true,
|
||||
"satisfied": true
|
||||
}
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 首次把门禁从「不满足」配到「满足」时(即上面「请求示例」一次性提交两行的那次调用)响应形状与上两例相同,字段取值为 `finalPlanPublished=true`、`finalPlanNotPublishedReason=null`、`pickupDropoffGate.satisfied=true`、`missingPickupDates`/`missingDropoffDates` 均为空数组(实测记录见 `wf/8429/ac2/result.md` AC-2 第二步,本节不重复整段 JSON)。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无额外空态。门禁不满足或未跃迁时 `finalPlanPublished=false` + 原因;拉回失败时 `requirementReopened=true` + 拉回失败原因。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
实测原文(`wf/8429/ac2/out/pd_ac4_pd3.json`,声明车数=2 时追加第 3 辆):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602202,
|
||||
"message": "2027-09-05 的 ARRIVAL 接机 已有 3 条派车行标记参与(派车行 2104183651266863105,2104183651463995394,2104183651883425793), 超过接送机需求声明的 2 辆",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
换版把声明车数降为 1 后,同样 2 辆车即触发(`wf/8429/ac2/out/pd_ac4_pd2_after_reduce.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 602202,
|
||||
"message": "2027-09-05 的 ARRIVAL 接机 已有 2 条派车行标记参与(派车行 2104183651266863105,2104183651463995394), 超过接送机需求声明的 1 辆",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 602202 | **语义改**:TRANSFER 需求,同日同方向标「参与」的派车行数 **超过声明车数**(改前是固定「超过 1」,现改按 `RequirementSnapshot.fleet[].count` 动态判定);失败时整批原子回滚(实测复核:失败调用前后 SQL 标记值完全一致);文案含日期、方向、标记条数、派车行 ID 列表、声明车数(见上两例) |
|
||||
| 605905 | 需求版本过期:锁内重读的当前生效需求 ID 或版本与写命令不一致(**不变**) |
|
||||
| 605913 | 接送机配置无效:`items[].assignmentId` 对应的派单行不存在、不属于当前需求,或已是终态(已取消/无车无司机)(**不变**) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **602202 的「标记参与」判据**:跳过 CANCELED 与 EXCEPTION 行,COMPLETED 照计(已完结日重新配置时它仍代表现状)。
|
||||
- **TRAVEL 需求不受 602202 约束**(TRANSFER-only),同日多行参与接机仍合法。
|
||||
- **count 缺失或 ≤0 时**:602202 上限按 1 计(与改前行为等价)。
|
||||
- **改前配置数据里已完结日**:越窗不再阻止发布;该行仍需满足其他完整性条件(有车有司机、形态合法等)。
|
||||
- **`items` 是全量覆盖语义**:请求里未列出的行,其 pickup/dropoff 标记会被隐式重置为 `false`;只想改动部分行时也必须把其余仍需保留标记的行一并带上,否则会被静默清零(实测 `wf/8429/ac2/result.md` 第 85 行记录同一现象)。
|
||||
- **`kind` 省略时的解析行为**(`#8453`,纯 TRANSFER 订单实测确认的既有缺陷,非本次改动引入):不传 `kind` 时 `RequirementKindResolver.resolveForMutation` 按 `TRAVEL` 解析当前生效需求;若该订单没有 TRAVEL 需求,解析结果为 `currentRequirement=null`,依赖它的校验(含 602202)会被静默跳过而不报错,接口返回**看似正常的 200**且 `finalPlanNotPublishedReason=PICKUP_DROPOFF_GATE_DISABLED`——该原因值本意是"这条需求 kind 不启用接送机门禁",用在这里具有误导性(实际是 kind 解析失配,不是真的门禁禁用)。实测对照(同一份 3 行 `items`、同一账号重放):
|
||||
- 漏传 `kind`(`wf/8429/ac2/run_ac4_pd3.log` 20:19:37):`http=200 code=200 finalPlanPublished=False reason=PICKUP_DROPOFF_GATE_DISABLED`,602202 判定被跳过。
|
||||
- 补传 `kind=TRANSFER`(`wf/8429/ac2/run_ac4_pd3b.log` 20:23:21):同一份 `items` 立即 `http=200 code=602202`,报文与上方错误响应一致。
|
||||
|
||||
**纯 TRANSFER 订单调用本接口必须显式传 `kind=TRANSFER`**,否则会静默绕过声明车数上限校验且不产生任何报错信号。该问题已登记为独立缺陷跟踪 `#8453`;在该单修复上线前,前端调用侧须将 `kind` 作为纯 TRANSFER 场景的强制字段随每次请求携带。
|
||||
|
||||
---
|
||||
|
||||
### 3. 需求确认 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`
|
||||
|
||||
**VO**: `ConfirmRequirementReqVO` → `ConfirmRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
车务最终确认需求(第④步)。改后新增未发布原因字段,已完结行越窗不再阻止发布。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| requirementId | Path | Long | ✅ | 用车需求 ID | **不变** |
|
||||
| orderId | Body | Long | ✅ | `@NotNull` | 订单 ID |
|
||||
| requestId | Body | String | ✅ | `@NotBlank` `@Size(max=64)` | 幂等请求标识 |
|
||||
| expectedRequirementVersion | Body | Integer | ✅ | `@NotNull` | 预期当前有效用车需求版本 |
|
||||
| expectedRequirementSha256 | Body | String | ✅ | `@NotBlank` `@Pattern(^[0-9a-f]{64}$)` | Board 返回的当前用车需求 canonical SHA-256 |
|
||||
| expectedPlanGeneration | Body | Long | ✅ | `@NotNull` | 预期当前最终派车方案代际 |
|
||||
| groups | Body | List<GroupDecisionVO> | ✅ | `@NotEmpty` `@Size(max=50)` | 当前有效执行段**精确集合**及各段行程短信选择;漏传/多传任一执行段触发 605056(见下方错误响应与业务边界) |
|
||||
| groups[].assignmentGroupId | Body | Long | ✅ | `@NotNull` | 当前有效派车组 ID |
|
||||
| groups[].sendItinerarySms | Body | Boolean | ✅ | `@NotNull` | 是否向本执行段司机发送行程短信 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | Long | **不变**:用车需求 ID |
|
||||
| dispatchPlanGeneration | Long | **不变**:已确认的最终派车方案代际 |
|
||||
| confirmed | Boolean | **不变**:整组是否原子确认成功 |
|
||||
| finalPlanPublished | Boolean | **不变**:本次是否发布了最终方案;`false` 表示确认已成功但订单车控仍为处理中(接送机未配齐或方案未派满),需按 605914/605915 提示补齐后重试 |
|
||||
| finalPlanNotPublishedReason | String | **新增**(#8429)。五个通用值,发布时为 `null`(本接口不含 pd 专属的两个值) |
|
||||
| groups | List<GroupResultVO> | **不变**:各执行段确认结果 |
|
||||
| groups[].assignmentId | Long | 代表派单 ID |
|
||||
| groups[].assignmentGroupId | Long | 派车组 ID;历史行无 `assignmentGroupId` 时回退下发 `assignmentId`,对任何真实行恒非空 |
|
||||
| groups[].assignmentStatus | String | 派单状态;实测恒为小写 `assigned`(#5827 提交即派定后,确认阶段不再有其他取值) |
|
||||
| groups[].confirmedAt | LocalDateTime | 车务最终确认时间;实测序列化为 `"yyyy-MM-dd HH:mm:ss"`(无 `T` 分隔符,见下方响应示例) |
|
||||
| groups[].sendItinerarySms | Boolean | 是否选择发送本段行程短信 |
|
||||
| groups[].itinerarySmsEventId | Long | 行程短信 Outbox 事件 ID;未发送为 `null` |
|
||||
| groups[].itinerarySmsStatus | String | 行程短信状态 |
|
||||
| groups[].itineraryUrl | String | 本段电子行程单 H5 链接;签发不可用时为 `null` |
|
||||
|
||||
**finalPlanNotPublishedReason 取值**(五个值,同 batch):
|
||||
|
||||
| 取值 | 触发条件 |
|
||||
|------|----------|
|
||||
| `STALE_FINALIZED_PLAN` | 存在按旧需求定稿的陈旧行 |
|
||||
| `INVALID_PLAN_GENERATION` | 方案代际不一致 |
|
||||
| `PLAN_INCOMPLETE` | 满派拓扑不完整 |
|
||||
| `CAPACITY_INSUFFICIENT` | 未定稿分支载客量不足 |
|
||||
| `GATE_UNSATISFIED` | 大交通要求的接/送机日未配车 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
实测原文(`wf/8429/ac2/out/confirm_req_body.json`,AC-5,groups 精确传当前 2 个 assigned 执行段):
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": 2104161383220457474,
|
||||
"requestId": "fleetqa-ac5-confirm-8337937430",
|
||||
"expectedRequirementVersion": 4,
|
||||
"expectedRequirementSha256": "24a2233c35565f31d14ecaaba4cf47abc56c592b5a0bb6b78f927ba4a949ca95",
|
||||
"expectedPlanGeneration": 362569745205170176,
|
||||
"groups": [
|
||||
{
|
||||
"assignmentGroupId": 362556592543109120,
|
||||
"sendItinerarySms": false
|
||||
},
|
||||
{
|
||||
"assignmentGroupId": 362569161085423616,
|
||||
"sendItinerarySms": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
确认成功且方案发布——实测原文(`wf/8429/ac2/out/confirm_resp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "2104178895018459138",
|
||||
"dispatchPlanGeneration": "362569745205170176",
|
||||
"confirmed": true,
|
||||
"finalPlanPublished": true,
|
||||
"finalPlanNotPublishedReason": null,
|
||||
"groups": [
|
||||
{
|
||||
"assignmentId": "2104166776231403521",
|
||||
"assignmentGroupId": "362556592543109120",
|
||||
"assignmentStatus": "assigned",
|
||||
"confirmedAt": "2026-09-27 19:10:28",
|
||||
"sendItinerarySms": false,
|
||||
"itinerarySmsEventId": null,
|
||||
"itinerarySmsStatus": "NOT_SENT",
|
||||
"itineraryUrl": null
|
||||
},
|
||||
{
|
||||
"assignmentId": "2104179344782008321",
|
||||
"assignmentGroupId": "362569161085423616",
|
||||
"assignmentStatus": "assigned",
|
||||
"confirmedAt": "2026-09-27 20:00:24",
|
||||
"sendItinerarySms": false,
|
||||
"itinerarySmsEventId": null,
|
||||
"itinerarySmsStatus": "NOT_SENT",
|
||||
"itineraryUrl": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 确认成功但未发布(`confirmed=true, finalPlanPublished=false, finalPlanNotPublishedReason="GATE_UNSATISFIED"` 等五值之一)的响应形状与上例相同,只是这三个字段取值不同——本轮测试未构造出该分支的原始抓包,字段定义见上方出参表与源码 `ConfirmRequirementRespVO.java`,取值语义见「finalPlanNotPublishedReason 取值」表。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无额外空态。确认成功但未发布时返回 `confirmed=true` + `finalPlanPublished=false` + 原因。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
按日志重建(`wf/8429/ac2/ac2.log:48`,字段形状与本文档其余错误响应一致的 `Result` 包装):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605056,
|
||||
"message": "执行段集合已变化,请刷新后重试",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 605056 | `groups` 未精确等于当前有效执行段集合(多传/少传任一均拒绝,非 602202/#8429 引入,**不变**但实测新确认;文档 `@ApiOperation` 未列出此码,属既有文档缺口)—— 实测:首次误传全部 6 个历史执行段(含已 completed)报本码,改传当前 2 个 assigned 执行段后成功 |
|
||||
| 605914 | 大交通要求接机的日期未配置接机车辆(**不变**) |
|
||||
| 605915 | 大交通要求送机的日期未配置送机车辆(**不变**) |
|
||||
| 605062 | 存在派车日期越出当前需求日期窗的在途槽位行(**不变**) |
|
||||
| 605037 / 605038 | 车辆维保或停用 / 司机休假或待激活,不可派(**不变**) |
|
||||
| 605059 | 同一 `requestId` 用于了不同的确认内容,须换新 `requestId` 重试(**不变**) |
|
||||
| 605063 | 原子确认回执已损坏,无法幂等重放;该码不可自愈,前端不得自动重试或静默轮询,须提示用户联系管理员(**不变**) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **确认与发布分离**:确认本身与最终方案发布是两件事,`confirmed=true` 不代表 `finalPlanPublished=true`;前端以两个字段组合判断下一步。
|
||||
- **已完结行越窗**:不再拒绝确认;确认成功但未发布时按原因(通常是门禁不满足或方案不完整)回到前面的步骤补齐。
|
||||
- **行程短信与电子行程单**:只在 `finalPlanPublished=true` 时发送(不变)。
|
||||
- **`groups` 必须是精确集合,不是增量**:提交前须先取当前全部有效执行段(如通过看板/详情接口)再据其构造 `groups`,多传已完结的历史执行段或少传遗漏均触发 605056;本次实测已验证该约束真实存在(见上方错误响应)。
|
||||
- **605063 终态**:命中该码时前端应停止对同一 `requestId` 的自动重试,引导人工介入,重试不会改变结果。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 判断发布结果
|
||||
|
||||
对所有三个接口,推荐判断顺序:
|
||||
|
||||
```
|
||||
1. 若 finalPlanPublished = true
|
||||
→ 最终方案已发布,订单车控推进到 DONE(不变)
|
||||
2. 若 finalPlanPublished = false 且 reason = GATE_UNSATISFIED
|
||||
→ 接送机未配齐,回到第③步配接送机
|
||||
3. 若 reason = PLAN_INCOMPLETE
|
||||
→ 派车方案不完整(缺车、缺司机等),回到第②步调整派车
|
||||
4. 若 reason = STALE_FINALIZED_PLAN 或 INVALID_PLAN_GENERATION
|
||||
→ 需重新确认需求(第④步 confirm),或方案代际变化需重新派车
|
||||
5. 若 reason = NO_GATE_TRANSITION(仅 pd)
|
||||
→ 本次未触发新的跃迁,重复保存或调整门禁再试
|
||||
```
|
||||
|
||||
### 前端不需要做的
|
||||
|
||||
- ❌ 按 `finalPlanPublished=false` 推断"哪里出了问题",必须用 `finalPlanNotPublishedReason` 字段精确判断。
|
||||
- ❌ 拦截错误码后自行决定前端跳转(跳转策略参考上面的判断顺序,但最权威的来源是后端原因字段)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无 DDL。改动全在判据(读侧)与新字段(响应序列化)。
|
||||
|
||||
| 对象 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 派车行(已完结) | 越窗时满派判据返 false,方案永不发布 | 越窗时 dateValid 豁免,其他条件仍检查 |
|
||||
| 派车行(在途) | 越窗时满派判据返 false | 不变 |
|
||||
| 最终方案快照 | 发布条件只看门禁(batch 路径),或门禁+满派(pd/confirm 路径) | 三条路径统一走完整判据 |
|
||||
| finalPlanNotPublishedReason 响应字段 | 无 | **新增**;发布时为 null;不发布时为首个未通过的原因 |
|
||||
| 602202 上限 | 1(TRANSFER-only) | max(1, 需求声明车数)(TRANSFER-only) |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
### 能否从 true 变成 false
|
||||
|
||||
- **能**(方案不完整场景):改前 batch 可能因只看门禁而返 true,改后补上满派检查可能返 false。前端需捕获这个变化。
|
||||
- **缓解**:原因字段让前端精确提示运营下一步;越窗已完结行豁免缓解了大部分场景。
|
||||
|
||||
### 已完结行的豁免形式
|
||||
|
||||
- **只豁免 `dateValid`**(日期窗口检查);不豁免其他条件(有车有司机、形态合法、每个 key 恰一条)。
|
||||
- **只豁免 COMPLETED 行**;HOLDING / ASSIGNED 越窗照旧拒。
|
||||
- **不影响其他判据**(陈旧定稿、方案代际、载客量、门禁都不变)。
|
||||
|
||||
### 版本联动
|
||||
|
||||
本单改动不涉及其他服务。下游 order-v3 对 finalPlanPublished 的消费逻辑不变(true 时更新车控为 DONE,false 时保持原状)。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| batch / pd / confirm `finalPlanNotPublishedReason` | 无 | **新增**;发布时 null,不发布时为首个未通过判据的原因(五个通用值或两个 pd 专属值) |
|
||||
| pd `finalPlanPublished`(已完结行越窗场景) | false | true(已完结行越窗不再阻止) |
|
||||
| batch `finalPlanPublished`(必须配车的服务日本次未提交 `dailyPlan` 行覆盖) | true | false + `PLAN_INCOMPLETE`(补上满派检查) |
|
||||
| 602202 上限 | 1(TRANSFER-only) | max(1, 需求声明车数)(TRANSFER-only) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 三个接口发布判据 | batch 只过门禁;pd/confirm 过满派+陈旧定稿+门禁 | 三者走同一判据:陈旧定稿→代际→满派→门禁 |
|
||||
| 越窗已完结行 | 满派判据返 false,方案永不发布 | 豁免 dateValid;其他条件仍检查 |
|
||||
| batch 存在必须配车服务日未提交 `dailyPlan` 行覆盖 | 门禁满足时发布(true) | 满派检查拦住,不发布(false + PLAN_INCOMPLETE) |
|
||||
| 602202 文案(TRANSFER) | 「同一天同一方向只能有一条」 | 「{日期} 的 {方向} 已有 {已标条数} 条…超过接送机需求声明的 {声明车数} 辆」 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。所有接口路径、入参、出参结构不变;错误码与文案(602202 除外)不变。只是三个接口新增一个字段,且 batch 的发布结果在「方案不完整」时取值会改变。
|
||||
- **前端是否必须同步上线**: 否。不改前端时:
|
||||
- 管理员的调用流程与改前一致(只是 batch 时 false 需要按新原因字段判断下一步,而原来的 false 本身就很少出现);
|
||||
- 新字段为 null 时前端可以忽略(安全字段,只在不发布时有值)。
|
||||
- **前端 workaround 清理点**:
|
||||
- 如果前端有"batch 返 false 就兜底改 pd"之类的旧补偿逻辑,需以新原因字段为准判断是「接送机未配」还是「方案不完整」,而不是盲目走第③步。
|
||||
- 602202 的提示文案可从「不能多配」改为「不能超过声明车数」(文案已自解释)。
|
||||
- 已完结行越窗的"方案卡死"问题随之消失,如有针对性的降级/提示可撤。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 其他接口:fleet 其他调用方、order-v3、mp 端、网关路由全不受影响。
|
||||
- 操作链路:第①②③④步的入参、错误码(除 602202 上限外)、取消/删除操作一概不变。
|
||||
- 数据结构:派车行、最终方案快照、需求实体的存储结构不变。
|
||||
- 已发布的快照:改前发的最终方案快照不重放,不在版本号上变化。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST,hl-fleet-service `267ab6906` 于 2026-09-27 19:40:15 部署,hl-gateway `71def6dc5` 同期在线(`deploy-status` 于 20:00 前后与 20:25:46 两次核对读数一致,全程无重新部署)。以下均为 2026-09-27 19:58-20:27 网关实测(测试专用车务账号,role=车务),原始请求/响应/SQL/日志见 `wf/8429/ac2/result.md` 及同目录 `out/*.json`。
|
||||
|
||||
### AC-2:越窗已完结行不再卡发布,batch 与 pd 答案一致 —— 已验证 通过
|
||||
|
||||
三步链路(同一需求 `requirementId=2104178895018459138`,换版后 serviceDates 含新日期 2027-08-30):
|
||||
1. **batch** 提交 [(08-15,车), (08-30,车)]:`finalPlanPublished=false, finalPlanNotPublishedReason=GATE_UNSATISFIED`;fleet 日志同步打出「接送机未配齐,暂不发布最终方案快照」。
|
||||
2. **pd** 一次性补标 08-15+08-30 两日接机:`finalPlanPublished=true, finalPlanNotPublishedReason=null`,`pickupDropoffGate.satisfied=true`;此后日志不再出现「未配齐」提示。
|
||||
3. **原样重放 batch**(20:02:43,未提交新行):仍 `finalPlanPublished=true, finalPlanNotPublishedReason=null`。
|
||||
|
||||
SQL 复核(20:02:43 之后):该需求名下 6 行全部 `dispatch_plan_finalized=1`,含越窗已完结行(08-03/08-04,`plan_finalized_requirement_id` 已指向当前需求)——越窗已完结行豁免、仍计入当前需求判定,符合预期。batch 与 pd 对同一份数据不再出现「一真一假」。
|
||||
|
||||
### AC-4:602202 改按声明车数动态判定 —— 已验证 通过
|
||||
|
||||
声明车数=2 时(需求 `2104183379455049729`,3 行落库 assigned):2 辆标接机 `http=200 code=200`;追加第 3 辆 → `http=200 code=602202`,文案含日期(2027-09-05)、方向(ARRIVAL)、3 个派车行 ID、声明车数「2」;失败调用前后 SQL 标记值完全一致,确认整批原子回滚。换版把声明车数降为 1(需求 `2104185762864115714`)后,同样 2 辆车即触发 602202,文案里的声明车数同步变为「1」。两级阈值均实测复现,判据已从固定值改为动态读需求声明车数。
|
||||
|
||||
### AC-5:三写口统一暴露 finalPlanNotPublishedReason —— 已验证 通过
|
||||
|
||||
- **batch**(即 AC-2 第一步):字段值 `GATE_UNSATISFIED`。
|
||||
- **pd**(AC-2 第二步同配置换新 requestId 重放,20:09:26):字段值 `NO_GATE_TRANSITION`(门禁已满足,重复提交不构成新的跃迁,不重新发布)。
|
||||
- **confirm**:`groups` 精确传当前 2 个 assigned 执行段后,响应 `data` 中存在 `finalPlanNotPublishedReason` 键,值为 `null`(已发布状态),见「三、接口详情」需求确认小节的响应示例。
|
||||
|
||||
三个写口的响应体均带该字段,取值随各自判据结果变化,字段本身在「发布」与「未发布」两种结果下都稳定出现。
|
||||
|
||||
### 已知边界
|
||||
|
||||
- **旧回执重放**(二期未上生产,仅测试服):本版本前生成的 confirm 回执若重放,可能出现 `finalPlanPublished=false` 而原因为 `null`。前端以 `finalPlanPublished` 为准,不要用「原因是否为空」反推是否已发布。
|
||||
- **count 缺失**:按 1 计,与改前等价。
|
||||
- **confirm 的 `groups` 必须精确等于当前有效执行段集合**:本轮实测中途误传过全部历史执行段(含已 completed),触发 605056,详见「三、接口详情」需求确认小节的错误响应。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|-----------|
|
||||
| — | #7067 | 统一发布入口,batch 补充门禁漏斗 | ⚠️ batch 返工时漏了满派与陈旧定稿守卫,本单补齐 |
|
||||
| — | #5810 | 已完结行随平移与豁免(基线、代际) | ⚠️ 满派拓扑这一处漏了,本单补齐 |
|
||||
| — | #7443 | 602202 首次引入(上限 1) | ⚠️ 上限被本单改为声明车数 |
|
||||
| **本 PR #8451** | **#8429** | 三个写口统一判据、已完结行越窗豁免、602202 按声明车数、新增未发布原因字段 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- **Issue**: [#8429](https://git.1814.love/wx/HL/issues/8429)
|
||||
- **PR**: [#8451](https://git.1814.love/wx/HL/pulls/8451)
|
||||
- **上游缺口**: [#5810](https://git.1814.love/wx/HL/issues/5810)(已完结行口径)、[#7067](https://git.1814.love/wx/HL/issues/7067)(发布统一)、[#7443](https://git.1814.love/wx/HL/issues/7443)(602202 来源)
|
||||
- **本轮实测新确认的既有缺陷**: [#8453](https://git.1814.love/wx/HL/issues/8453)(接送机配置 `kind` 省略时绕过 605905/602202/门禁判据,纯 TRANSFER 订单需显式传 `kind`;非本单引入,详见「三、接口详情」批量派车与接送机配置两节的业务边界)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8429](https://git.1814.love/wx/HL/issues/8429)
|
||||
- **PR**: [#8451](https://git.1814.love/wx/HL/pulls/8451)
|
||||
- **Merge commit**: [267ab6906](https://git.1814.love/wx/HL/commit/267ab6906)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,303 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8430"
|
||||
title: "纯接送机订单看板详情改按接送机需求(TRANSFER)取数:门禁要求日、逐日方案、车数、进度、方案代际"
|
||||
consumer: "admin"
|
||||
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: "PR #8452 已合入 dev-v3(squash 0bae84b59)。TEST 环境 hl-fleet-service 0bae84b59 于 2026-09-27 20:29:52 部署、21:12 与 21:24 两次复核未变,hl-gateway 71def6dc5;同一张纯接送机订单在部署前(fleet 267ab6906)与部署后各取一次详情做对照,结论见第八节。只改看板详情一个接口,看板列表未改;TRAVEL 与 TRANSFER 并存的订单行为不变。 前端实证(mmg 2026-09-28):结构/字段名/类型不变只变取值,前端已读 requirementIdentities[kind=TRANSFER](#7443/#7990),dispatchReadOnly 前端零消费,纯接送机订单详情自动有数据;判 not_required。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 纯接送机订单看板详情改按接送机需求(TRANSFER)取数
|
||||
|
||||
**服务**: hl-fleet-service
|
||||
**PR**: `#8452`(已合入 `dev-v3`,squash `0bae84b59`)
|
||||
**Issue**: #8430
|
||||
**日期**: 2026-09-27
|
||||
**影响范围**: 管理后台车务看板「订单详情」,只影响**纯接送机订单**(只有 TRANSFER 用车需求、没有 TRAVEL 需求)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🟡 **纯接送机订单的详情终于有内容了。** 改前这类订单打开详情,取数仍按(并不存在的)TRAVEL 需求走:`dailyVehiclePlan` 恒为空数组、`suggestedVehicleCount`/`actualVehicleCount` 恒为 0、进度停在第 1 步、接送机门禁的要求日取成了订单出发日与返回日。改后这些字段都按 TRANSFER 需求取值。**响应结构、字段名、字段类型都没有变**,变的只是纯接送机订单上这些字段的取值。
|
||||
|
||||
🟡 **顶层 `requirementId` / `requirementVersion` / `requirementSha256` 对纯接送机订单仍为 null**(设计如此)。要拿接送机需求的身份(例如调需求级确认接口),请用 `requirementIdentities` 里 `kind=TRANSFER` 那一条。
|
||||
|
||||
🟡 **纯接送机订单现在也可能返回 605311。** 这个错误码 #7990 就有,此前只会出现在 TRAVEL 订单上;改后同一判据也作用于 TRANSFER 需求,前端按已有的 605311 处理即可。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
纯接送机订单(只下了接送机、没有行程用车)在车务看板打开详情时,后端 `BoardOrderService.queryOrderDetail` 只按 TRAVEL 需求取数。TRAVEL 缺席,于是窗口、门禁、逐日方案、车数、进度全部落空或落到订单快照兜底,车务在详情里看不到任何已派的车,也拼不出需求级确认要回传的方案代际。
|
||||
|
||||
本单改为:TRAVEL 缺席时,以 TRANSFER 需求作为「有效需求」取数。TRAVEL 存在时有效需求就是 TRAVEL,逐字沿用原逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 车务看板订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 修改(取值) | 纯接送机订单按 TRANSFER 需求取数;结构不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 车务看板订单详情 `GET /admin/fleet/board/orders/{orderId}`
|
||||
|
||||
**VO**: `BoardOrderDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台车务看板,点开一张订单的详情抽屉。纯接送机订单现在能看到逐日派车方案、实派车数、接送机缺口与进度,并能从 `requirementIdentities` 拿到接送机需求的版本、指纹与方案代际,用于需求级确认。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Path | Long | 是 | 订单主键(雪花 ID) | 看板列表行里的订单 ID |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
只列本单改变取值的字段,其余字段与改前一致。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String | **取值不变**:纯接送机订单仍为 null(只指向 TRAVEL 需求) |
|
||||
| requirementVersion | Integer | **取值不变**:纯接送机订单仍为 null |
|
||||
| requirementSha256 | String | **取值不变**:纯接送机订单仍为 null |
|
||||
| requirementIdentities | List | 每条需求一个身份;纯接送机订单只有一条 `kind=TRANSFER`。**改后**其 `dispatchPlanGeneration` 按该需求自己的已定稿行计算(改前纯接送机订单恒为 null) |
|
||||
| requirementIdentities[].dispatchPlanGeneration | String | 该需求已定稿行的唯一方案代际;该需求还没有已定稿行、或已定稿行没有代际时为 null,前端按可空处理 |
|
||||
| pickupDropoffGate.arrivalRequiredDates | List | **改后**取自 TRANSFER 需求声明的接机服务日;改前取成订单出发日 |
|
||||
| pickupDropoffGate.departureRequiredDates | List | **改后**取自 TRANSFER 需求声明的送机服务日;改前取成订单返回日 |
|
||||
| pickupDropoffGate.missingPickupDates / missingDropoffDates | List | 按上面的要求日与实际标记参与的派车行重新计算 |
|
||||
| dailyVehiclePlan | List | **改后**返回 TRANSFER 需求的逐日派车行;改前纯接送机订单恒为空数组。已完结的行 `readOnly=true`、`readOnlyReason="派单已完结"` |
|
||||
| suggestedVehicleCount | Integer | **改后**按 TRANSFER 需求计算;改前恒为 0 |
|
||||
| actualVehicleCount | Integer | **改后**按 TRANSFER 需求的派车行计算;改前恒为 0 |
|
||||
| progressSteps | List | **改后**按 TRANSFER 需求的派车行推进;改前停在第 1 步 |
|
||||
| dispatchReadOnly | Boolean | 判据没变:行程已结束,或逐日方案**每一行**都已完结、服务日已过或已关账,满足其一即为 true。**改后**纯接送机订单的逐日方案有了行,第二条也会对它生效;改前它只在行程已结束时为 true |
|
||||
| dispatchReadOnlyReason | String | 行程已结束时为「行程已结束」;逐日方案全部只读时为「全部服务日期已过去、完结或关账」;不只读时为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/fleet/board/orders/2104161383220457474
|
||||
Authorization: Bearer <车务账号 token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服实测(纯接送机订单,部署后,截取关键字段;`dailyVehiclePlan` 共 7 行,只列两行):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"orderNo": "HL20260927184901765",
|
||||
"teamNo": "26-9179",
|
||||
"startDate": "2027-08-03",
|
||||
"endDate": "2027-08-09",
|
||||
"requirementId": null,
|
||||
"requirementVersion": null,
|
||||
"requirementSha256": null,
|
||||
"requirementIdentities": [
|
||||
{
|
||||
"kind": "TRANSFER",
|
||||
"requirementId": "2104185762864115714",
|
||||
"requirementVersion": 5,
|
||||
"requirementSha256": "36bc63bf38bc881a7f5a48222538f232a304600cd970627247729262983b78e0",
|
||||
"dispatchPlanGeneration": "362573468547551232"
|
||||
}
|
||||
],
|
||||
"suggestedVehicleCount": 1,
|
||||
"actualVehicleCount": 3,
|
||||
"dispatchReadOnly": false,
|
||||
"pickupDropoffGate": {
|
||||
"arrivalRequiredDates": ["2027-08-30", "2027-09-05"],
|
||||
"departureRequiredDates": ["2027-08-09"],
|
||||
"missingPickupDates": ["2027-08-30"],
|
||||
"missingDropoffDates": [],
|
||||
"declared": true,
|
||||
"satisfied": false
|
||||
},
|
||||
"progressSteps": [
|
||||
{"step": 1, "code": "ORDER_DETAIL", "status": "DONE"},
|
||||
{"step": 2, "code": "DISPATCH", "status": "DONE"},
|
||||
{"step": 3, "code": "PICKUP_DROPOFF", "status": "PROCESSING"},
|
||||
{"step": 4, "code": "CONFIRM_EXECUTE", "status": "WAITING"}
|
||||
],
|
||||
"dailyVehiclePlan": [
|
||||
{
|
||||
"serviceDate": "2027-08-03",
|
||||
"assignmentId": "2104162972542935041",
|
||||
"planFinalized": true,
|
||||
"planState": "USED",
|
||||
"pickupParticipant": true,
|
||||
"assignmentStatus": "completed",
|
||||
"readOnly": true,
|
||||
"readOnlyReason": "派单已完结"
|
||||
},
|
||||
{
|
||||
"serviceDate": "2027-09-05",
|
||||
"assignmentId": "2104183651266863105",
|
||||
"planFinalized": false,
|
||||
"planState": "UNPLANNED",
|
||||
"pickupParticipant": true,
|
||||
"pickupRequired": true,
|
||||
"assignmentStatus": "assigned",
|
||||
"readOnly": false,
|
||||
"readOnlyReason": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 纯接送机订单还没派过车:`dailyVehiclePlan` 为空数组、`actualVehicleCount=0`、`requirementIdentities[TRANSFER].dispatchPlanGeneration=null`,门禁要求日照常按 TRANSFER 需求给出。
|
||||
- TRAVEL 与 TRANSFER 两条需求都取不到(订单服务不可达,`relatedDetailReady=false`):与改前完全一致,顶层字段走订单快照兜底。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 605311,
|
||||
"message": "当前需求存在多个不透明派车方案代际,请联系车务核对派车方案后重试",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 |
|
||||
|---|---|
|
||||
| 605311 | 有效需求(纯接送机订单即 TRANSFER 需求)的已定稿派车行出现一个以上方案代际,或无代际的老定稿行与有代际的行混在一起。看板不猜当前代际,直接返回该码(HTTP 200)。错误码本身不是新增的(#7990),本单之后纯接送机订单也会走到这个判据 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 只改看板**详情**;看板**列表**未改。同一张纯接送机订单在列表与详情里的派车状态、车辆、需求身份已实测一致;列表的 `currentStep` 按 3 步编号、详情的 `progressSteps` 按 4 步编号,两者本来就不同源,不是本单引入的差异。
|
||||
- TRAVEL 与 TRANSFER 并存的订单:有效需求就是 TRAVEL,每个字段的取值与改前逐字相同。
|
||||
- 打开详情不会改变 TRANSFER 需求的状态(只有 TRAVEL 需求会在打开详情时被置为处理中,这一点没变)。
|
||||
- `dispatchReadOnly` 的判据没变(行程已结束,或逐日方案每一行都已完结、服务日已过或已关账),但纯接送机订单现在有了逐日方案行,所以「全部完结」的纯接送机订单行程未结束也会整单只读,原因「全部服务日期已过去、完结或关账」。单行是否只读看 `dailyVehiclePlan[].readOnly`。
|
||||
- 纯接送机订单调需求级确认接口时,`expectedRequirementVersion` / `expectedRequirementSha256` / `expectedPlanGeneration` 取 `requirementIdentities` 中 `kind=TRANSFER` 那一条;`dispatchPlanGeneration` 为 null 时说明该需求还没有唯一的已定稿方案,此时不能按「已定稿方案」确认。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
纯接送机订单的需求身份一律从 `requirementIdentities` 取,不要读顶层 `requirementId`:
|
||||
|
||||
```js
|
||||
const transfer = (detail.requirementIdentities || []).find(x => x.kind === 'TRANSFER');
|
||||
// 纯接送机订单:顶层 requirementId 为 null,身份一律从 transfer 取
|
||||
const expected = transfer && {
|
||||
expectedRequirementVersion: transfer.requirementVersion,
|
||||
expectedRequirementSha256: transfer.requirementSha256,
|
||||
expectedPlanGeneration: transfer.dispatchPlanGeneration, // 可能为 null
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无 DDL、无写入。本单只改详情接口的内存组装,打开详情不写任何表。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
### 605311 的触发面变大
|
||||
|
||||
判据本身没变(有效需求的已定稿行只能有一个方案代际,老的无代际定稿行不能与有代际的行混在一起),变的是它现在也作用于纯接送机订单的 TRANSFER 需求。前端沿用 605311 现有的提示即可,不需要新分支。
|
||||
|
||||
### 方案代际为 null
|
||||
|
||||
`requirementIdentities[TRANSFER].dispatchPlanGeneration` 在该需求还没有已定稿行、或已定稿行没有代际时为 null。这时详情照常返回,只是不能按「已定稿方案」做需求级确认。
|
||||
|
||||
### 只读判据
|
||||
|
||||
`dispatchReadOnly` 的判据没变,有两条,满足其一即为 true:
|
||||
|
||||
1. 行程已结束(订单返回日早于今天),原因「行程已结束」;
|
||||
2. 逐日方案每一行都已完结、服务日已过或服务日已关账,原因「全部服务日期已过去、完结或关账」。
|
||||
|
||||
改前纯接送机订单的逐日方案恒为空,第 2 条对它永不成立;改后它有了逐日方案行,TRANSFER 的派车行全部完结时,即使行程还没结束,整单也会只读。实测订单还有三行在途,所以为 `false`。单行只读看 `dailyVehiclePlan[].readOnly`,已完结行为 `true`、原因「派单已完结」。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
同一张纯接送机订单实测:
|
||||
|
||||
| 字段 | 改前(fleet `267ab6906`) | 改后(fleet `0bae84b59`) |
|
||||
|------|------|------|
|
||||
| `pickupDropoffGate.arrivalRequiredDates` | `["2027-08-03"]`(订单出发日) | `["2027-08-30","2027-09-05"]`(TRANSFER 需求的接机服务日) |
|
||||
| `pickupDropoffGate.departureRequiredDates` | `["2027-08-09"]`(订单返回日) | `["2027-08-09"]` |
|
||||
| `dailyVehiclePlan` | 0 行 | 7 行(4 行已完结 + 09-05 三行已派) |
|
||||
| `suggestedVehicleCount` / `actualVehicleCount` | 0 / 0 | 1 / 3 |
|
||||
| `progressSteps` 状态 | 进行中 / 待处理 / 待处理 / 待处理 | 已完成 / 已完成 / 进行中 / 待处理 |
|
||||
| `requirementIdentities[TRANSFER].dispatchPlanGeneration` | null | `362573468547551232` |
|
||||
| 顶层 `requirementId` | null | null(不变) |
|
||||
|
||||
送机要求日改前改后恰好相同(订单返回日刚好也是 TRANSFER 需求的送机日),区分改前改后的是接机要求日。
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| 接口结构 | 无变化,不新增、不删除字段 |
|
||||
| 纯接送机订单 | 详情字段从空值 / 0 / 订单日期兜底变为按 TRANSFER 需求取值;可能新出现 605311;派车行全部完结时 `dispatchReadOnly` 会变为 true |
|
||||
| TRAVEL 订单、TRAVEL 与 TRANSFER 并存订单 | 无变化 |
|
||||
| 看板列表 | 无变化 |
|
||||
| 数据库 | 无 DDL、无写入 |
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 看板列表、派车 / 改派 / 接送机配置 / 需求级确认等写接口:未改。
|
||||
- TRAVEL 订单、TRAVEL 与 TRANSFER 并存的订单:取值不变。
|
||||
- 响应结构:没有新增或删除字段。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST,hl-fleet-service `0bae84b59`(2026-09-27 20:29:52 部署,21:12 与 21:24 两次复核未变),hl-gateway `71def6dc5`,测试专用车务账号。
|
||||
|
||||
1. **同一订单改前改后对照**(第六节):部署前在 fleet `267ab6906` 上取详情,接机要求日为订单出发日、逐日方案 0 行、车数 0/0;部署后同一订单接机要求日为 TRANSFER 服务日、逐日方案 7 行、车数 1/3,`requirementIdentities[TRANSFER]` 带方案代际。
|
||||
2. **接送机配置写口与详情门禁一致**:另建一张只有送机方向的纯接送机订单,接送机配置接口响应里的门禁与随后看板详情返回的门禁逐字段相同(`arrivalRequiredDates=[]`、`departureRequiredDates=["2027-10-19"]`、`satisfied=true`);只有送机方向时接机要求日为空,不会拿订单出发日硬凑。
|
||||
3. **拿详情里的身份做需求级确认**:用同一订单详情里 `requirementIdentities[TRANSFER]` 的需求 ID、版本、指纹、代际调 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 成功(`confirmed=true`、`finalPlanPublished=true`),服务端接受了详情给出的四个值。
|
||||
4. **列表与详情一致**:同一订单在看板列表(`GET /admin/fleet/board/orders`)中的派车状态、车辆、需求身份与详情一致。
|
||||
5. **单测**:看板详情新增 7 条纯接送机订单用例(含全部已完结、旧版在途行、平移重绑行、同一需求多代 605311、不置处理中、顶层身份为 null),合并后在 dev-v3 上复跑 234/0 通过。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- PR [#8451](https://git.1814.love/wx/HL/pulls/8451)(#8429):接送机最终方案发布三写口统一判据,本单实测纯接送机订单详情里的门禁与它的接送机配置写口响应逐字段一致。
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- **Issue**: [#8430](https://git.1814.love/wx/HL/issues/8430)
|
||||
- **PR**: [#8452](https://git.1814.love/wx/HL/pulls/8452)
|
||||
- **前序**: [#7990](https://git.1814.love/wx/HL/issues/7990)(`requirementIdentities` 与 605311 的来源)、[#8429](https://git.1814.love/wx/HL/issues/8429)(接送机最终方案发布判据)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,907 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8436"
|
||||
title: "团期退单审批 GB-ADM-072~075 放开团期管理员;074 审批通过后退款单不再自动审核,以 PENDING 进退款审批中心人工审;退单详情与团期审批中心列表的 canApprove 按退单专用门判定"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "681518ec8cec55f0502fc3bb8622a00a0627e5f6"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "PR #8441 已合入 dev-v3(合并提交 c97c0b4ca),2026-09-27 17:25 部署 TEST。五个接口(GB-ADM-072/073/074/075 与团期审批中心列表 GB-ADM-061)的路径、入参、出参结构、错误码与文案全部不变;变的是退单审批人新增团期管理员、canApprove 取值、actualRefundAmount 语义改为应退额,以及 074 通过后退款单停在 PENDING 进退款审批中心、须再调 POST /v3/admin/refund/review 审核才打款。前端需:①退单审批页与团期审批中心页的 isAdmin 整页门禁放行团期管理员,否则团期管理员进不了页面;②退单详情「实退金额」改按应退额展示,最终打款以退款审批中心批复为准;③审批按钮继续只按 canApprove 显隐。两个 nacos 回滚开关缺省均为开。TEST(c97c0b4ca)已验收:团期管理员可列表 / 详情 / 审批通过;FULL_DEPOSIT 退款单以 PENDING 进退款审批中心,calculatedAmount=订金额,人工同意不填金额按订金额落 actualAmount;两个开关置 false 均实测生效并已还原。【2026-09-27 mmg】前端已交付(681518ec):用户拍板两页都放——withdraw/index 与 group-batch-approval/index 整页门禁 isAdmin 扩为 canAccess=isAdmin||isGroupBatchManager;退单详情「实退金额」改「应退金额」+批复为准提示(详情页与 WithdrawApprovalDetailModal 两处),074 成功提示与通过弹窗 alert 补「退款已转退款审批中心待审」;按钮显隐零改(#8253 已按 canApprove)。三 spec 共 18 例全绿,checkpoint 全绿。遗留:团期管理员角色的两页菜单入口需后端 sys_menu 配置。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期退单审批放开团期管理员 + 批后退款进退款审批中心二审
|
||||
|
||||
**服务**: hl-order-service-v3(端口 8086/8186)
|
||||
**PR**: `#8441`(已合入 `dev-v3`,合并提交 `c97c0b4ca`)
|
||||
**Issue**: #8436
|
||||
**日期**: 2026-09-27
|
||||
**影响范围**: 管理后台退单审批页(GB-ADM-072~075)与团期审批中心列表(GB-ADM-061)的审批人范围、按钮显隐,以及退单审批通过后退款的去向
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **退单审核通过(GB-ADM-074)后不再直接放款。** 改前:审批通过即自动审核退款单,退款审批中心的待审列表里看不到这张单。改后:子订单照常取消、名额照常回退,但退款单以 `PENDING`(待审核)进入退款审批中心,须有人再调 `POST /v3/admin/refund/review` 审核通过后才打款。
|
||||
|
||||
🔴 **团期管理员(`GROUP_BATCH_MANAGER`)可以审批退单。** GB-ADM-072~075 的审批人由 {`ADMIN`, `SUPER_ADMIN`} 扩为 {`GROUP_BATCH_MANAGER`, `ADMIN`, `SUPER_ADMIN`},允许自审(提交人与审批人可为同一人)。流团审批不变,团期管理员仍不能批流团。
|
||||
|
||||
🟡 **`canApprove` 取值变化**:团期管理员调退单详情(073)或团期审批中心列表(061)时,待审退单的 `canApprove` 由 `false` 变为 `true`;流团行仍为 `false`。
|
||||
|
||||
🟡 **退单详情 `actualRefundAmount` 语义变化**:它是审批通过时按当时团期状态算出的**应退额**,最终打款额以退款审批中心的批复为准(审核人可部分同意或驳回)。数值算法没变。
|
||||
|
||||
🟢 **五个接口的路径、入参、出参结构、错误码与文案全部不变。** `589530` 文案仍是「仅管理员可处理退单审核」。
|
||||
|
||||
⚠️ **前端门禁**:hl-ui 退单审批页(`src/views/order/withdraw/index.vue`)与团期审批中心页(`src/views/order/group-batch-approval/index.vue`)最外层按 `isAdmin` 整页门禁,只认 `ADMIN` / `SUPER_ADMIN` 角色,团期管理员打开这两页既不渲染也不发请求。后端已放开,页面入口需前端放行。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
- jw 2026-09-27 定案:团期退单的**审批侧**由团期管理员处理,允许自审;钱的出口另由退款审批中心把关(二审)。
|
||||
- 退单是「提交 → 审批」两段(#7100)。本单只改审批侧;提交侧(GB-ADM-070 谁能提交)归 #7608 批 3,本单未改——团期管理员提交退单仍返回 `581008`。
|
||||
- 两处放开互相绑定:团期管理员能批退单的前提是退款审批中心还有一道闸。退款审核端点本就拒绝团期管理员(`581008`),所以团期管理员批完退单后,钱必须由其他有退款审核权的角色审出去。二审开关一旦关掉,团期管理员的退单审批权随之收回(见「六、边界行为」)。
|
||||
- 改成人工二审后暴露了一个退款金额问题:订金全额退(`FULL_DEPOSIT`)的退款单,退款审批中心展示的应退额(`calculatedAmount`)原来是政策预览额(已付 × 政策比例),不是订金。以前自动审核时显式传入了订金金额,所以没暴露;改成人工审后,审核人不填金额直接同意就会多退。本单一并修正:此类退款单的 `calculatedAmount` 改写为取消时算定的应退额。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | GB-ADM-072 退单审批分页列表 | GET | `/v3/admin/order/group-batch/withdraw/page` | 修改(判权) | 放行团期管理员;响应结构与取值不变 |
|
||||
| 2 | GB-ADM-073 退单申请详情 | GET | `/v3/admin/order/group-batch/withdraw/:approvalId` | 修改(判权、取值口径) | 放行团期管理员;`canApprove` 改按退单专用门;`actualRefundAmount` 语义改为应退额 |
|
||||
| 3 | GB-ADM-074 退单审核通过 | POST | `/v3/admin/order/group-batch/withdraw/:approvalId/approve` | 修改(判权、行为) | 放行团期管理员;通过后退款单以 PENDING 进退款审批中心;响应结构不变 |
|
||||
| 4 | GB-ADM-075 取消退单(驳回) | POST | `/v3/admin/order/group-batch/withdraw/:approvalId/reject` | 修改(判权) | 放行团期管理员;响应结构与驳回行为不变 |
|
||||
| 5 | GB-ADM-061 团期审批中心列表 | GET | `/v3/admin/order/group-batch/approvals/page` | 修改(取值口径) | 退单行 `canApprove` 改按退单专用门,团期管理员可为 true;判权与结构不变 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. GB-ADM-072 退单审批分页列表 `GET /v3/admin/order/group-batch/withdraw/page`
|
||||
|
||||
**VO**: `WithdrawApprovalListReqVO` → `Result<PageResult<WithdrawApprovalItemRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
退单审批页主列表,缺省只列待审单。**本单起团期管理员也可调用**,其余用法不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalStatus | Query | String | ❌ | `PENDING` / `APPROVED` / `REJECTED` / `ALL` | 缺省 `PENDING`。**行为不变** |
|
||||
| groupBatchId | Query | Long | ❌ | 团期 ID | **行为不变** |
|
||||
| keyword | Query | String | ❌ | — | 客户姓名 / 订单号模糊。**行为不变** |
|
||||
| createdFrom | Query | String | ❌ | `yyyy-MM-dd` | 提交时间起。**行为不变** |
|
||||
| createdTo | Query | String | ❌ | `yyyy-MM-dd`,含当日 | 提交时间止。**行为不变** |
|
||||
| pageNo | Query | Integer | ❌ | 默认 1 | 页码(本接口叫 `pageNo`,审批中心列表叫 `pageNum`)。**行为不变** |
|
||||
| pageSize | Query | Integer | ❌ | 默认 20,最大 100 | **行为不变** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `PageResult<WithdrawApprovalItemRespVO>` | **结构完全不变**:`records` / `total` / `page` / `pageSize` |
|
||||
| records[] | `WithdrawApprovalItemRespVO` | 字段与取值均不变:approvalId、groupBatchId、batchNo、batchName、batchLabel、productName、departDate、orderId、teamNo、orderNo、customerName、customerPhoneMasked、participantCount、affectedOrderCount、paidAmount、estimatedRefundAmount、refundMode、refundModeName、reason、applicantName、approvalStatus、approvalStatusName、createdAt |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无请求体。
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/withdraw/page?pageNo=1&pageSize=20&keyword=HL20260927163133498 HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <审批人 token:ADMIN / SUPER_ADMIN / GROUP_BATCH_MANAGER>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
记录取自 TEST 2026-09-27 实测响应(订单 2,ADMIN 调用);团期管理员调用返回同一结构。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"approvalId": "2104127004276359170",
|
||||
"groupBatchId": "2104126740546949121",
|
||||
"batchNo": "Q202612292104126714147975170",
|
||||
"batchName": "#8436-退单审批验收",
|
||||
"batchLabel": "168",
|
||||
"productName": "冻干粉发短信给",
|
||||
"departDate": "2026-12-29",
|
||||
"orderId": "2104126787711860738",
|
||||
"teamNo": "26-6441",
|
||||
"orderNo": "HL20260927163133498",
|
||||
"customerName": "测试八四三六乙",
|
||||
"customerPhoneMasked": "138****4362",
|
||||
"participantCount": 2,
|
||||
"affectedOrderCount": 1,
|
||||
"paidAmount": 6000.00,
|
||||
"estimatedRefundAmount": 1000.00,
|
||||
"refundMode": "FULL_DEPOSIT",
|
||||
"refundModeName": "订金全额退",
|
||||
"reason": "#8436 验收造数:退单审批(订单2)",
|
||||
"applicantName": "cw_test_8006_x",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvalStatusName": "待审批",
|
||||
"createdAt": "2026-09-27 16:32:25"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时返回空页:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**降级**:nacos 开关 `group-batch.acl.allow.withdraw-approver-group-batch-manager` 或 `group-batch.withdraw.refund-second-review` 任一置 `false` 时,团期管理员回到改前口径,调本接口返回 `589530`;`ADMIN` / `SUPER_ADMIN` 不受影响。两个开关缺省均为 `true`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589530,
|
||||
"message": "仅管理员可处理退单审核",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 | 本单 |
|
||||
|---|---|---|
|
||||
| 589530 | 当前角色不在 {`ADMIN`, `SUPER_ADMIN`, `GROUP_BATCH_MANAGER`};或团期管理员处于回滚态;或请求带不出角色 | 放行集合新增团期管理员;码与文案不变 |
|
||||
| 401 | 缺少有效 Authorization(网关拦截,响应体 code,HTTP 仍为 200) | 不变 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 放行规则:`ADMIN` / `SUPER_ADMIN` 恒放行;`GROUP_BATCH_MANAGER` 在两个开关都开时放行;定制师、财务、房务、车务等仍 `589530`。
|
||||
- 团期管理员看到的是**全部团期**的退单,不按「本人负责的团期」过滤——系统里还没有团期与团期管理员的归属关系。
|
||||
- 本接口不返回 `canApprove`;按钮显隐以 073 详情为准。
|
||||
|
||||
---
|
||||
|
||||
### 2. GB-ADM-073 退单申请详情 `GET /v3/admin/order/group-batch/withdraw/:approvalId`
|
||||
|
||||
**VO**: `Result<WithdrawApprovalDetailRespVO>`(继承 `WithdrawApprovalItemRespVO`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
退单审批页或团期审批中心退单行打开详情;弹窗的「同意 / 取消退单」按钮按 `canApprove` 显隐,分别调 074 / 075。**本单起团期管理员也可调用,且其 `canApprove` 可为 true。**
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | 退单审批单 ID | 须为退单类型,否则 `589531`。**行为不变** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| canApprove | Boolean | **取值口径变化**:PENDING、过**退单审批**角色门(本单起含团期管理员)、团期与订单都存在、且团期仍可退团时为 `true`;恒非 null。团期管理员调用时由改前的拿不到(`589530`)变为可见且可为 `true` |
|
||||
| actualRefundAmount | BigDecimal | **语义变化**:审批通过时按当时团期状态算出的**应退额**(通过后回填,未通过为 null)。本单起退款单进退款审批中心二审,最终打款额以退款审批中心批复为准;数值算法不变 |
|
||||
| refundApplicationId | Long | 不变,恒为 null(退款单在审批提交后异步建);要找退款单请按 `orderId` 查退款审批中心 |
|
||||
| 其余字段 | — | 与 072 `records[]` 相同的 23 个字段,加 currentEstimatedRefundAmount、currentRefundMode、refundPolicy、consultantName、approvedByName、approvedAt、approveRemark,结构与取值不变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无请求体。
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/withdraw/2104127004276359170 HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <审批人 token:ADMIN / SUPER_ADMIN / GROUP_BATCH_MANAGER>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
TEST 2026-09-27 实测响应(订单 2,ADMIN 调用);团期管理员在两个开关都开时调用,返回同一结构且 `canApprove` 同为 `true`。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"approvalId": "2104127004276359170",
|
||||
"groupBatchId": "2104126740546949121",
|
||||
"batchNo": "Q202612292104126714147975170",
|
||||
"batchName": "#8436-退单审批验收",
|
||||
"batchLabel": "168",
|
||||
"productName": "冻干粉发短信给",
|
||||
"departDate": "2026-12-29",
|
||||
"orderId": "2104126787711860738",
|
||||
"teamNo": "26-6441",
|
||||
"orderNo": "HL20260927163133498",
|
||||
"customerName": "测试八四三六乙",
|
||||
"customerPhoneMasked": "138****4362",
|
||||
"participantCount": 2,
|
||||
"affectedOrderCount": 1,
|
||||
"paidAmount": 6000.00,
|
||||
"estimatedRefundAmount": 1000.00,
|
||||
"refundMode": "FULL_DEPOSIT",
|
||||
"refundModeName": "订金全额退",
|
||||
"reason": "#8436 验收造数:退单审批(订单2)",
|
||||
"applicantName": "cw_test_8006_x",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvalStatusName": "待审批",
|
||||
"createdAt": "2026-09-27 16:32:25",
|
||||
"currentEstimatedRefundAmount": 1000.00,
|
||||
"currentRefundMode": "FULL_DEPOSIT",
|
||||
"refundPolicy": null,
|
||||
"actualRefundAmount": null,
|
||||
"consultantName": "admin",
|
||||
"approvedByName": null,
|
||||
"approvedAt": null,
|
||||
"approveRemark": null,
|
||||
"refundApplicationId": null,
|
||||
"canApprove": true
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无空态。团期或订单查不到时对应字段为 null、`canApprove=false`;团期已推进到出行中及以后时 `currentEstimatedRefundAmount` / `currentRefundMode` 为 null、`canApprove=false`(均不变)。
|
||||
|
||||
**降级**:两个开关任一置 `false` 时,团期管理员调本接口返回 `589530`(与 072 相同)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589531,
|
||||
"message": "退单申请不存在",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 | 本单 |
|
||||
|---|---|---|
|
||||
| 589530 | 同 072 | 放行集合新增团期管理员;码与文案不变 |
|
||||
| 589531 | 退单申请不存在 / 已软删 / 非退单类型 | 不变 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- `canApprove` 与 074 用同一道判定:详情显示可审批,074 的角色门就一定放行;按钮显隐只看它,不要前端按角色自判。
|
||||
- `canApprove=true` 时点同意,仍可能因团期状态刚推进而收到 `589501`,以 074 返回为准(不变)。
|
||||
- `actualRefundAmount` 只代表退单审批这一步算出的应退额;实际打款额看退款审批中心该退款单的审核结果。
|
||||
|
||||
---
|
||||
|
||||
### 3. GB-ADM-074 退单审核通过 `POST /v3/admin/order/group-batch/withdraw/:approvalId/approve`
|
||||
|
||||
**VO**: `ApproveWithdrawReqVO` → `Result<WithdrawApprovalDetailRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
审批人点「同意」:取消该户子订单、名额 −1 户 / −N 人、生成退款单。**本单起两处变化**:团期管理员也可调用;生成的退款单不再自动审核,停在 `PENDING` 进入退款审批中心,由有退款审核权的角色调 `POST /v3/admin/refund/review` 审核后才打款。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | 退单审批单 ID | **行为不变** |
|
||||
| remark | Body | String | ❌ | ≤512 字;请求体可整体省略 | 批复备注。**行为不变** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `WithdrawApprovalDetailRespVO` | **结构完全不变**,同 073。通过后 `approvalStatus=APPROVED`、`approvalStatusName=已通过`、`actualRefundAmount` 为应退额、`canApprove=false`、`refundApplicationId` 恒 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/withdraw/2104126999897542657/approve HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <审批人 token:ADMIN / SUPER_ADMIN / GROUP_BATCH_MANAGER>
|
||||
Content-Type: application/json
|
||||
|
||||
{"remark": "已与客户确认,同意退单"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
TEST 2026-09-27 16:33 实测响应(订单 1,ADMIN 调用)。本单未改响应,同一请求改后返回同一结构与取值;差别在响应之外的退款单状态,见「业务边界」。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"approvalId": "2104126999897542657",
|
||||
"groupBatchId": "2104126740546949121",
|
||||
"batchNo": "Q202612292104126714147975170",
|
||||
"batchName": "#8436-退单审批验收",
|
||||
"batchLabel": "168",
|
||||
"productName": "冻干粉发短信给",
|
||||
"departDate": "2026-12-29",
|
||||
"orderId": "2104126740412731393",
|
||||
"teamNo": "26-9318",
|
||||
"orderNo": "HL20260927163122212",
|
||||
"customerName": "测试八四三六甲",
|
||||
"customerPhoneMasked": "138****4361",
|
||||
"participantCount": 2,
|
||||
"affectedOrderCount": 1,
|
||||
"paidAmount": 6000.00,
|
||||
"estimatedRefundAmount": 1000.00,
|
||||
"refundMode": "FULL_DEPOSIT",
|
||||
"refundModeName": "订金全额退",
|
||||
"reason": "#8436 验收造数:退单审批(订单1)",
|
||||
"applicantName": "cw_test_8006_x",
|
||||
"approvalStatus": "APPROVED",
|
||||
"approvalStatusName": "已通过",
|
||||
"createdAt": "2026-09-27 16:32:24",
|
||||
"currentEstimatedRefundAmount": 1000.00,
|
||||
"currentRefundMode": "FULL_DEPOSIT",
|
||||
"refundPolicy": null,
|
||||
"actualRefundAmount": 1000.00,
|
||||
"consultantName": "admin",
|
||||
"approvedByName": "ha_r1_ad",
|
||||
"approvedAt": "2026-09-27 16:33:16",
|
||||
"approveRemark": "#8436 改前证据:旧代码 074 审批通过",
|
||||
"refundApplicationId": null,
|
||||
"canApprove": false
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无空态。应退额为 0 的退单不建退款单,`actualRefundAmount=0.00`(不变)。
|
||||
|
||||
**降级**:
|
||||
- `group-batch.withdraw.refund-second-review` 置 `false`:退款单回到改前行为,建单即自动审核通过、不进退款审批中心待审列表,退款单的 `calculatedAmount` 也回到政策预览额(改前口径),实退金额仍按取消时算定的金额;**同时团期管理员失去退单审批权**(072~075 均 `589530`),避免团期管理员一人批完即放款。
|
||||
- `group-batch.acl.allow.withdraw-approver-group-batch-manager` 置 `false`:团期管理员调本接口 `589530`;退款二审照常。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589532,
|
||||
"message": "该退单申请已处理,不可重复操作",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 | 本单 |
|
||||
|---|---|---|
|
||||
| 589530 | 同 072 | 放行集合新增团期管理员;码与文案不变 |
|
||||
| 589531 | 退单申请不存在 | 不变 |
|
||||
| 589532 | 申请已处理;或并发下已被他人处理 | 不变 |
|
||||
| 589500 | 团期不存在 | 不变 |
|
||||
| 589501 | 团期状态已不允许退团(出行中及以后) | 不变 |
|
||||
| 589512 | 子订单不属于该团期 | 不变 |
|
||||
| 400 | `remark` 超过 512 字 | 不变 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **退款单去向**:状态 `PENDING`,出现在退款审批中心待审列表,申请人类型 `SYSTEM`。审核走 `POST /v3/admin/refund/review`;团期管理员调退款审核返回 `581008`「无权查看此订单」——团期管理员能批退单、批不了退款,钱须由其他有退款审核权的角色审出去。
|
||||
- **退款单应退额**(退款审批中心的 `calculatedAmount`):`FULL_DEPOSIT`(团期招募中 / 资源准备中)= 订金应付额,与本接口 `actualRefundAmount` 一致;`POLICY`(物资准备中 / 待出发)= 已付 × 政策比例,口径不变。审核人不填 `approvedAmount` 直接同意时,按 `calculatedAmount` 退。
|
||||
- **找退款单**:本接口响应的 `refundApplicationId` 恒为 null,请按 `orderId` 查 `GET /v3/admin/refund/application/page`(可带 `status=PENDING`)。
|
||||
- 取消原因恰好以「申诉」开头时,退款单的 `reasonText` 前加「取消原因:」,防止被退款域当作申诉单;订单侧的取消原因不变。
|
||||
- 团期时间线中本步写入的描述由「子订单 #X 退单审核通过,实退 N」改为「子订单 #X 退单审核通过,退款 N 已转退款审批中心待审」;应退为 0 或二审开关关闭时保持原文。
|
||||
- 允许自审;审批单分别记录提交人与批复人(不变)。
|
||||
- 并发:同一审批单同时批,只有一个成功,另一个 `589532`(不变)。
|
||||
|
||||
---
|
||||
|
||||
### 4. GB-ADM-075 取消退单(驳回) `POST /v3/admin/order/group-batch/withdraw/:approvalId/reject`
|
||||
|
||||
**VO**: `RejectWithdrawReqVO` → `Result<WithdrawApprovalDetailRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
审批人点「取消退单」:驳回申请,该户继续留在团期中走原流程。**本单起团期管理员也可调用**,驳回行为不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| approvalId | Path | Long | ✅ | 退单审批单 ID | **行为不变** |
|
||||
| remark | Body | String | ✅ | 非空白,≤512 字 | 驳回原因。**行为不变** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | `WithdrawApprovalDetailRespVO` | **结构完全不变**,同 073。驳回后 `approvalStatus=REJECTED`、`approvalStatusName=已取消退单`,`actualRefundAmount` / `refundApplicationId` 为 null,`canApprove=false` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/group-batch/withdraw/2104127012929208322/reject HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <审批人 token:ADMIN / SUPER_ADMIN / GROUP_BATCH_MANAGER>
|
||||
Content-Type: application/json
|
||||
|
||||
{"remark": "客户已改口,继续参团"}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
TEST 实测(2026-09-27 17:42,ADMIN 驳回造数订单 4 的退单申请):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"approvalId": "2104127012929208322",
|
||||
"groupBatchId": "2104126740546949121",
|
||||
"batchNo": "Q202612292104126714147975170",
|
||||
"batchName": "#8436-退单审批验收",
|
||||
"batchLabel": "168",
|
||||
"productName": "冻干粉发短信给",
|
||||
"departDate": "2026-12-29",
|
||||
"orderId": "2104126797589446658",
|
||||
"teamNo": "26-0687",
|
||||
"orderNo": "HL20260927163135910",
|
||||
"customerName": "测试八四三六丁",
|
||||
"customerPhoneMasked": "138****4364",
|
||||
"participantCount": 2,
|
||||
"affectedOrderCount": 1,
|
||||
"paidAmount": 1000.0,
|
||||
"estimatedRefundAmount": 1000.0,
|
||||
"refundMode": "FULL_DEPOSIT",
|
||||
"refundModeName": "订金全额退",
|
||||
"reason": "#8436 验收造数:退单审批(订单4)",
|
||||
"applicantName": "cw_test_8006_x",
|
||||
"approvalStatus": "REJECTED",
|
||||
"approvalStatusName": "已取消退单",
|
||||
"createdAt": "2026-09-27 16:32:27",
|
||||
"currentEstimatedRefundAmount": 1000.0,
|
||||
"currentRefundMode": "FULL_DEPOSIT",
|
||||
"refundPolicy": null,
|
||||
"actualRefundAmount": null,
|
||||
"consultantName": "admin",
|
||||
"approvedByName": "ha_r1_ad",
|
||||
"approvedAt": "2026-09-27 17:42:34",
|
||||
"approveRemark": "#8436 验收造数收尾",
|
||||
"refundApplicationId": null,
|
||||
"canApprove": false
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无空态。驳回不退款,`actualRefundAmount` / `refundApplicationId` 恒 null(不变)。
|
||||
|
||||
**降级**:两个开关任一置 `false` 时,团期管理员调本接口返回 `589530`(与 072 相同)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589530,
|
||||
"message": "仅管理员可处理退单审核",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 | 本单 |
|
||||
|---|---|---|
|
||||
| 589530 | 同 072 | 放行集合新增团期管理员;码与文案不变 |
|
||||
| 589531 | 退单申请不存在 | 不变 |
|
||||
| 589532 | 申请已处理 | 不变 |
|
||||
| 400 | `remark` 为空白或超过 512 字 | 不变 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 驳回只改审批单状态,订单、名额、钱三项零变动;该户可再次提交退单(不变)。
|
||||
- 团期管理员可以驳回任一团期的退单(不按团期归属过滤,同 072)。
|
||||
|
||||
---
|
||||
|
||||
### 5. GB-ADM-061 团期审批中心列表 `GET /v3/admin/order/group-batch/approvals/page`
|
||||
|
||||
**VO**: `GroupBatchApprovalListReqVO` → `Result<PageResult<GroupBatchApprovalItemRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期审批中心主列表,一页同时展示流团(`DISBAND`)与退单户(`WITHDRAW`)两类申请;前端按每行 `canApprove` 决定是否显示「同意 / 拒绝」。**本单只改退单行 `canApprove` 的取值口径**,判权、入参、出参结构不变。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| bizType | Query | String | ❌ | `DISBAND` / `WITHDRAW` | 不传 = 两类都要。**行为不变** |
|
||||
| approvalStatus | Query | String | ❌ | `PENDING` / `APPROVED` / `REJECTED` | 不传 = 全部。**行为不变** |
|
||||
| groupBatchId | Query | Long | ❌ | 团期 ID | **行为不变** |
|
||||
| batchName | Query | String | ❌ | 空白视为不传 | 团期名称包含匹配;「第N期」/「N」另按期号精确匹配。**行为不变** |
|
||||
| pageNum | Query | Integer | ❌ | ≥1,默认 1 | **行为不变** |
|
||||
| pageSize | Query | Integer | ❌ | 1–100,默认 20 | **行为不变** |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| records[].canApprove | Boolean | **取值口径变化**:按行类型分门。`WITHDRAW` 行 = 过**退单审批**角色门(`ADMIN` / `SUPER_ADMIN`,本单起另含两个开关都开时的 `GROUP_BATCH_MANAGER`)且 PENDING、团期存在且仍可退团;`DISBAND` 行 = 过**流团审批**角色门(仍只 `ADMIN` / `SUPER_ADMIN`)且其余条件不变。恒非 null |
|
||||
| records[] 其余字段 | — | 结构与取值不变:approvalId、bizType、bizTypeName、bizTypeText、groupBatchId、batchNo、batchName、batchLabel、approvalStatus、approvalStatusName、orderId、affectedOrderCount、participantCount、estimatedRefundAmount、reason、applicantName、approvedByName、approvedAt、createTime、orderNo、teamNo、customerName、customerPhoneMasked、departDate |
|
||||
| total / page / pageSize | Long / Integer / Integer | 分页包装,不变;列表字段名是 `records` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
无请求体。
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/approvals/page?bizType=WITHDRAW&approvalStatus=PENDING&pageNum=1&pageSize=20 HTTP/1.1
|
||||
Host: api.test.1814.love
|
||||
Authorization: Bearer <持 group-batch:view 的账号 token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
示例:字段结构与 TEST 实测一致(团期管理员按团期 `2104126740546949121` 过滤调用,code 200);为展示待审行,这一行的审批状态与 `canApprove` 按造数订单 2 审批前的状态示意。实测对照:全表唯一一条待审退单行,团期管理员看到 `canApprove=true`、定制师看到 `false`;TEST 上没有待审的流团行,团期管理员对流团行恒为 `false` 由单测覆盖。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"approvalId": "2104127004276359170",
|
||||
"bizType": "WITHDRAW",
|
||||
"bizTypeName": "退单户",
|
||||
"bizTypeText": "退单户",
|
||||
"groupBatchId": "2104126740546949121",
|
||||
"batchNo": "Q202612292104126714147975170",
|
||||
"batchName": "#8436-退单审批验收",
|
||||
"batchLabel": "168",
|
||||
"approvalStatus": "PENDING",
|
||||
"approvalStatusName": "待审批",
|
||||
"orderId": "2104126787711860738",
|
||||
"affectedOrderCount": 1,
|
||||
"participantCount": 2,
|
||||
"estimatedRefundAmount": 1000.00,
|
||||
"reason": "#8436 验收造数:退单审批(订单2)",
|
||||
"applicantName": "cw_test_8006_x",
|
||||
"approvedByName": null,
|
||||
"approvedAt": null,
|
||||
"canApprove": true,
|
||||
"createTime": "2026-09-27 16:32:25",
|
||||
"orderNo": "HL20260927163133498",
|
||||
"teamNo": "26-6441",
|
||||
"customerName": "测试八四三六乙",
|
||||
"customerPhoneMasked": "138****4362",
|
||||
"departDate": "2026-12-29"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无命中时返回空页:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**降级**:两个开关任一置 `false` 时,团期管理员看到的退单行 `canApprove` 回到 `false`;列表本身照常返回(本接口判权不读这两个开关)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
| 错误码 | 触发条件 | 本单 |
|
||||
|---|---|---|
|
||||
| 589507 | 当前角色无 `group-batch:view` | 不变 |
|
||||
| 400 | `pageNum` < 1 或 `pageSize` 不在 1–100 | 不变 |
|
||||
| 401 | 缺少有效 Authorization(响应体 code,HTTP 仍为 200) | 不变 |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 判权不变:本接口走 `group-batch:view` 权限码(团期管理员持有),与审批端点的角色门不是同一道门。
|
||||
- 同一页里团期管理员看到的是:退单行可审批(`canApprove=true`,PENDING 且团期仍可退团时),流团行不可审批(`canApprove=false`)。前端按行读 `canApprove` 即可,不需要区分角色。
|
||||
- 角色门按请求算一次、按行类型取用,与行数无关(不变)。
|
||||
- 已处理(APPROVED / REJECTED)行 `canApprove` 恒 `false`(不变)。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### 按钮显隐
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| ✅ 同意 / 驳回按钮 | 只按 073 详情或 061 行上的 `canApprove === true` 显示 |
|
||||
| ❌ 前端按角色自判 | 会漏掉团期管理员,或给团期管理员的流团行错显按钮(点下去 `589547` / `589530`) |
|
||||
| ✅ `589530` 提示 | 直接透出 `message`;文案仍是「仅管理员可处理退单审核」,本单未改,不要按文案推断角色 |
|
||||
|
||||
### 审批通过 ≠ 已退款
|
||||
|
||||
| 步骤 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| ① 批退单 | `POST /v3/admin/order/group-batch/withdraw/:approvalId/approve` | 子订单取消、名额回退、建退款单(PENDING) |
|
||||
| ② 找退款单 | `GET /v3/admin/refund/application/page?orderId=<子订单ID>&status=PENDING` | 074 响应的 `refundApplicationId` 恒为 null,只能按 `orderId` 查 |
|
||||
| ③ 审退款 | `POST /v3/admin/refund/review` | body 例:`{"applicationId": "<退款单ID>", "decision": "APPROVED"}`;不传 `approvedAmount` 时按 `calculatedAmount` 退;团期管理员调用 `581008` |
|
||||
|
||||
### payload 对照
|
||||
|
||||
| 场景 | payload |
|
||||
|------|---------|
|
||||
| ✅ 074 不带备注 | 无请求体 |
|
||||
| ✅ 074 带备注 | `{"remark": "已与客户确认,同意退单"}` |
|
||||
| ✅ 075 驳回 | `{"remark": "客户已改口,继续参团"}` |
|
||||
| ❌ 075 不填原因 | `{}` 或 `{"remark": " "}` → 400 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
只写外部可观察的结果。零 DDL、零 Flyway、零数据迁移。
|
||||
|
||||
| 接口 / 对象 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 074 子订单 | 取消 | 不变 |
|
||||
| 074 团期名额 | −1 户 / −N 人 | 不变 |
|
||||
| 074 退单审批单 | 置已通过,回填批复人与应退额 | 不变 |
|
||||
| 074 退款单状态 | 建单即自动审核通过,自动审核记录的审核人为 074 审批人 | 建单停在 `PENDING`,无自动审核记录 |
|
||||
| 074 退款单应退额(`FULL_DEPOSIT`) | 政策预览额(已付 × 政策比例) | 取消时算定的订金应付额 |
|
||||
| 074 退款单应退额(`POLICY`) | 已付 × 政策比例 | 不变 |
|
||||
| 074 应退为 0 | 不建退款单 | 不变 |
|
||||
| 072 / 073 / 075 / 061 | — | 写入行为无变化 |
|
||||
|
||||
存量:本单前已自动审核通过的退款单不回溯。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截,不变)。
|
||||
- 有请求但角色头缺失 → `589530`(fail-closed,不变)。
|
||||
- 团期推进到出行中及以后 → 074 返回 `589501`,073 / 061 `canApprove=false`(不变)。
|
||||
- 应退为 0 → 只取消订单、不建退款单,时间线写「实退 0.00」(不变)。
|
||||
- 团期招募中 / 资源准备中(FULL_DEPOSIT)而该户已付少于订金(含已付 0)→ 应退额仍按订金算(存量口径,本单未改),退款单进审批中心后人工同意会被 `530404`「审核同意金额超出可退额度」拦下,需按实付改金额或驳回。
|
||||
- 回滚开关(nacos,缺省均为 `true`,热刷新):
|
||||
|
||||
| 开关 | 置 `false` 的效果 |
|
||||
|------|------|
|
||||
| `group-batch.acl.allow.withdraw-approver-group-batch-manager` | 团期管理员调 072~075 回到 `589530`;073 / 061 退单行对团期管理员的 `canApprove` 回到 `false`;退款二审照常 |
|
||||
| `group-batch.withdraw.refund-second-review` | 074 通过后退款单回到建单即自动审核通过,`calculatedAmount` 回到政策预览额(改前口径),实退仍按取消时算定的金额;**同时团期管理员失去退单审批权**,效果同上一行 |
|
||||
|
||||
两个开关只影响团期管理员这一条放行和退款去向;`ADMIN` / `SUPER_ADMIN` 恒放行,定制师、财务等恒拒,流团审批恒不放团期管理员,都不读开关。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### refundMode(`com.hulalv.order.core.enums.OrderRefundMode`)
|
||||
|
||||
**所属字段**: `WithdrawApprovalItemRespVO.refundMode` / `WithdrawApprovalDetailRespVO.currentRefundMode` | **类型**: `String`
|
||||
|
||||
团期退单只会产生下面两个值(由批复时团期状态决定,本单未改):
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `FULL_DEPOSIT` | 订金全额退 | 团期招募中 / 资源准备中;应退 = 订金应付额。本单起退款单应退额与之一致 |
|
||||
| `POLICY` | 按政策退 | 物资准备中 / 待出发;应退 = 已付 × 退改政策比例 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 073 `canApprove`(团期管理员调用) | 拿不到详情(`589530`) | PENDING 且团期可退团时 `true` |
|
||||
| 061 退单行 `canApprove`(团期管理员调用) | 恒 `false` | PENDING 且团期可退团时 `true` |
|
||||
| 061 流团行 `canApprove`(团期管理员调用) | `false` | `false`(不变) |
|
||||
| 073 / 074 `actualRefundAmount` 语义 | 实际退款额 | 应退额,最终以退款审批中心批复为准(数值不变) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期管理员调 072~075 | `589530` | 放行(两个开关都开时) |
|
||||
| 定制师 / 财务 / 房务 / 车务调 072~075 | `589530` | `589530`(不变) |
|
||||
| 074 通过后退款单状态 | 自动审核通过 | `PENDING`,进退款审批中心待审列表 |
|
||||
| 074 通过后打款时点 | 自动进入打款流程 | 退款审批中心审核通过之后 |
|
||||
| `FULL_DEPOSIT` 退款单应退额(已付 6000、订金 1000 的户) | 6000.00(政策预览额) | 1000.00(订金应付额) |
|
||||
| 团期时间线审批通过那条描述 | 「退单审核通过,实退 N」 | 「退单审核通过,退款 N 已转退款审批中心待审」 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。路径、入参、出参结构、错误码与文案全部不变;074 通过后钱不再自动退出是本单有意的行为变化。
|
||||
- **前端是否必须同步上线**: 否。不改前端时,管理员的用法与改前一致(只是退款多一道审核);团期管理员被页面 `isAdmin` 门禁挡在页外,用不到新能力。
|
||||
- **前端 workaround 清理点**:
|
||||
- 退单审批页与团期审批中心页的 `isAdmin` 整页门禁需放行团期管理员;审批中心页里流团行由 `canApprove=false` 自然不显示按钮,不需要额外判断。
|
||||
- 退单详情「实退金额」标签需改按应退额展示,并提示最终以退款审批中心批复为准。
|
||||
- 074 成功提示可补一句「退款已转退款审批中心」。
|
||||
- 团期管理员角色的退单审批菜单本单未绑定,需在菜单配置里给该角色挂上两页入口。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 退单审批四端点(GB-ADM-072~075)的放行角色,退单详情与审批中心列表的 `canApprove`,以及 074 通过后退款单的状态与应退额。
|
||||
- **零影响**:
|
||||
- GB-ADM-070 提交退单:判权不变,团期管理员仍 `581008`,其余角色需 `group-batch:withdraw:submit`(#7608 批 3)。
|
||||
- 流团审批(流团详情、GB-ADM-062 同意 / 拒绝、GB-ADM-060 提交):仍只放 `ADMIN` / `SUPER_ADMIN`。
|
||||
- 退款审批中心各接口(列表、详情、`POST /v3/admin/refund/review`):结构与判权不变,只是多出团期退单来的 `PENDING` 单。
|
||||
- C 端取消订单的退款:退款模式固定 `POLICY`,应退额口径不变;唯一可见变化是取消原因恰以「申诉」开头时退款单 `reasonText` 加前缀「取消原因:」。
|
||||
- 小程序端(`consumer: mp`):无接口变化。
|
||||
- 存量数据:本单前已自动审核的退款单不回溯。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**环境**:TEST,经业务网关 `https://api.test.1814.love`;身份为自签 token(`role` + `adminId`)。TEST 上没有持 `FINANCE` 角色的真实账号,财务用例为自签 `role=FINANCE`(审批角色门只认角色头,结论成立)。
|
||||
|
||||
### 8.1 改前(旧字节,2026-09-27 16:26~16:36)
|
||||
|
||||
**构建身份**:部署面板显示 order-v3 为 `dev-v3 @ 69046568c`(16:21:31 部署,不含本单)。扫运行中两个实例(8086 / 8186)所加载的 jar:本单新增的两个日志标签 `GB_WITHDRAW_APPROVER_GBM_DISABLED`、`GB_WITHDRAW_REFUND_SECOND_REVIEW_OFF` 均未命中,同一个类里跨版本稳定的 `GB_APPROVAL_ROLE_CLAIM_MISSING` 命中,说明未命中是版本旧、不是类没打进 jar。取证前后三次复核,两个进程 pid 不变。
|
||||
|
||||
**判权对照(072 / 073,审批单为造数订单 2 的待审单)**:
|
||||
|
||||
| 角色(adminId) | 072 列表 | 073 详情 |
|
||||
|---|---|---|
|
||||
| CUSTOMIZER(1002) | `589530`「仅管理员可处理退单审核」 | `589530` |
|
||||
| GROUP_BATCH_MANAGER(2102259564525301761) | `589530` | `589530` |
|
||||
| FINANCE(自签) | `589530` | `589530` |
|
||||
| ADMIN(2103791504101412865,阳性对照) | 200,命中 4 条待审 | 200,`canApprove=true` |
|
||||
|
||||
**074 改前行为**(订单 1:已付 6000、订金 1000,团期招募中;审批人 ADMIN ha_r1_ad,提交人为另一 ADMIN 账号):
|
||||
|
||||
```
|
||||
POST /v3/admin/order/group-batch/withdraw/2104126999897542657/approve
|
||||
→ 200,approvalStatus=APPROVED,refundMode=FULL_DEPOSIT,actualRefundAmount=1000.00,refundApplicationId=null ✓
|
||||
GET /v3/admin/refund/application/page?orderId=2104126740412731393&status=PENDING
|
||||
→ 200,total=0(旧字节下退款单不进待审列表)✓
|
||||
GET /v3/admin/refund/application/page?orderId=2104126740412731393
|
||||
→ 200,1 条:status=APPROVED,calculatedAmount=6000.00,actualAmount=1000.00,reviewerNameLast=ha_r1_ad ✓
|
||||
```
|
||||
|
||||
- 退款单建单后约 1 秒内即由 074 审批人自动审核通过(状态流水 PENDING→APPROVED,操作人为 074 审批人),约 2 分钟后复查仍为已通过、尚无打款记录。
|
||||
- 应退额错位实证:`calculatedAmount=6000.00` 是政策预览额(已付 6000 × 政策比例 100%),实际审批金额是订金 1000.00——改成人工二审后,审核人不填金额直接同意就会按 6000 退,这是本单一并修正的问题。
|
||||
- 团期时间线新增「子订单 #HL20260927163122212 退单审核通过,实退 1000.00」;订单时间线新增取消(CUSTOMIZING→CANCELLED)与系统「退款审核通过 ¥1000.00」两条。
|
||||
- 名额由 12 人 / 6 户回退为 10 人 / 5 户,团期仍为招募中。
|
||||
|
||||
### 8.2 改后(新字节 `c97c0b4ca`,2026-09-27 17:27~17:39)
|
||||
|
||||
**构建身份**:部署面板 `hl-order-service-v3 <- dev-v3 @ c97c0b4ca`(17:25:39)。两个实例(8086 / 8186)加载的 jar 都命中本单两个日志标签;逐 AC 共复核 8 次,两个进程 pid 始终不变,窗口内无人重新部署。
|
||||
|
||||
**判权(072 / 073,审批单为造数订单 2 的待审单,每种各打 2 次)**:
|
||||
|
||||
| 角色(adminId) | 072 列表 | 073 详情 |
|
||||
|---|---|---|
|
||||
| GROUP_BATCH_MANAGER(2102259564525301761) | **200**,total=4 | **200**,`approvalStatus=PENDING`,`canApprove=true` |
|
||||
| CUSTOMIZER(1002) | `589530`「仅管理员可处理退单审核」 | `589530` |
|
||||
| FINANCE(自签) | `589530` | `589530` |
|
||||
|
||||
**团期管理员审批通过 + 退款进审批中心**(订单 2:已付 6000、订金 1000,团期招募中 → FULL_DEPOSIT):
|
||||
|
||||
```
|
||||
POST /v3/admin/order/group-batch/withdraw/2104127004276359170/approve (团期管理员)
|
||||
→ 200,approvalStatus=APPROVED,actualRefundAmount=1000.00,refundApplicationId=null ✓
|
||||
订单 CUSTOMIZING→CANCELLED,团期名额 10 人 / 5 户 → 8 / 4 ✓
|
||||
GET /v3/admin/refund/application/page?orderId=2104126787711860738&status=PENDING
|
||||
→ 200,1 条:status=PENDING,calculatedAmount=1000.00,actualAmount=null ✓(改前同场景 total=0)
|
||||
POST /v3/admin/refund/review (ADMIN,decision=APPROVED,不填金额)
|
||||
→ 200,status PENDING→APPROVED,actualAmount=1000.00 ✓(未落成 6000)
|
||||
```
|
||||
|
||||
| 退款单字段 | 改后(订单 2) | 改前(订单 1,8.1) |
|
||||
|---|---|---|
|
||||
| 建单后状态 | **PENDING**,无审核记录 | APPROVED(074 审批人自动审核) |
|
||||
| applicant_type / refund_type | SYSTEM / DEPOSIT | SYSTEM / DEPOSIT |
|
||||
| **calculated_amount** | **1000.00**(取消时算定的订金额) | 6000.00(政策预览额) |
|
||||
| 人工同意后 actual_amount | 1000.00 | —(自动审核 1000.00) |
|
||||
|
||||
- 团期时间线:「子订单 #HL20260927163133498 退单审核通过,**退款 1000.00 已转退款审批中心待审**」(改前为「实退 1000.00」)。
|
||||
- 074 / 073 响应:外层 5 个键、`data` 33 个字段,集合与顺序与改前完全一致;取值差异只在订单号、客户、审批人、时间等造数身份字段。接口文档中 `actualRefundAmount` 描述已更新为应退额口径。
|
||||
|
||||
**自审**:ADMIN(2102028348437970945)对订单 5 自己提交(070)并自己审批通过(074),审批单 `applicant_id = approved_by_id`;退款单 PENDING、`calculated_amount=1000.00`。
|
||||
|
||||
**回滚开关**(nacos tenant=test `hl-order-service-v3-test.yml`,发布与还原都带 `casMd5`,还原后与原文 md5 `c2206934960057f70b7173159046dc54` / 7522 字节逐字节一致):
|
||||
|
||||
| 开关置 `false` | 生效 | 实测 |
|
||||
|---|---|---|
|
||||
| `group-batch.acl.allow.withdraw-approver-group-batch-manager` | 发布后约 5.6 秒 | 团期管理员调 072:网关 6/6 与两个实例直连均 `589530`;两台服务日志均有 `GB_WITHDRAW_APPROVER_GBM_DISABLED`;还原后约 3.3 秒恢复 200 |
|
||||
| `group-batch.withdraw.refund-second-review` | 发布后约 4.8 秒 | 团期管理员调 072 同样 `589530`;ADMIN 审批订单 3 后退款单**自动 APPROVED**、`actual_amount=1000.00`、`calculated_amount=6000.00`(回到改前口径),时间线写「实退 1000.00」,日志有 `GB_WITHDRAW_REFUND_SECOND_REVIEW_OFF`;还原后约 7.1 秒恢复 |
|
||||
|
||||
**旁证:招募中已付 0 的户**(订单 6):074 通过后退款单 PENDING、`paid_amount=0.00`、`calculated_amount=1000.00`;人工同意(不填金额)返回 `530404`「审核同意金额超出可退额度(已付¥0,已退¥0,本次¥1,000)」,随后驳回,终态 REJECTED。FULL_DEPOSIT 按订金算应退额是存量口径,二审把它拦在了打款之前。
|
||||
|
||||
**造数**:班期 `2104126714152169473`、团期 `2104126740546949121`(「#8436-退单审批验收」),子订单 6 张;整个团期无一张退款单执行打款,验收结束后待审的退单申请与退款单均已驳回。
|
||||
|
||||
### 8.3 本地证据
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| 定向单测(守卫 4 类、退单审批 / 退款 / 流团审批服务、两个审批控制器角色门、退款监听器与建单服务等) | rebase 到 `307e9efc7` 后 19 类 295 例全绿 |
|
||||
| order-v3 整模块全量(有 Docker,rebase 前) | 13967 例,失败 10 / 错误 8 / 跳过 7;8 个红类中 7 个在基底 `69046568c` 单跑同样红、红法一致,1 个为并发锁竞争时序抖动(本分支单跑 10/10 绿),均与本单无关 |
|
||||
| 应退为 0 的退单 | 单测覆盖:审批通过后时间线写「实退 0.00」、不写转审批中心,监听器不建退款单 |
|
||||
| 流团审批不放团期管理员 | 单测覆盖:流团审批重载对团期管理员恒拒且不读开关 |
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|----|-------|------|------------|
|
||||
| — | #7100 | 退单改为「提交 → 管理员审批」两段,审批通过即自动审核退款 | ⚠️ 退款自动审核部分已被本单改为二审 |
|
||||
| — | #7609 | 审批角色门对缺角色头 fail-closed | ✅ 有效 |
|
||||
| #8256 / #8260 | #8253 | 退单详情与审批中心列表引入 `canApprove` | ✅ 有效(本单改其退单行取值口径) |
|
||||
| #8438 | #7608 | 批 3:退单提交接 `group-batch:withdraw:submit` | ✅ 有效(提交侧,本单未改) |
|
||||
| **本 PR #8441** | **#8436** | 审批侧放开团期管理员 + 批后退款二审 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8436](https://git.1814.love/wx/HL/issues/8436)
|
||||
- 关联 PR: [wx/HL#8441](https://git.1814.love/wx/HL/pulls/8441)
|
||||
- 同组端点上一版契约:`changelogs-v2/2026-09/24_8253_团期审批中心统一审核补字段与团期名称搜索-修改接口-管理后台.md`
|
||||
- 提交侧判权:`changelogs-v2/2026-09/27_7608_团期退团提交与核单定稿判权收口批3-修改接口-管理后台.md`
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8436](https://git.1814.love/wx/HL/issues/8436)
|
||||
- **PR**: [#8441](https://git.1814.love/wx/HL/pulls/8441)
|
||||
- **Merge commit**: [c97c0b4ca](https://git.1814.love/wx/HL/commit/c97c0b4ca5348cceba55d0296f2c5e5623a8485a)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
@@ -0,0 +1,375 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8446"
|
||||
title: "订单列表与状态计数新增查询参数 productType(核心 / 定制 / 团期页签)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ab3b6be1268721e56ed809da9329b367d8b890e9"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-28"
|
||||
status_note: "PR #8454 已合入 dev-v3(22145977d),order-v3 已部署测试服,经网关实测三个端点。新增查询参数 productType,与其他条件取 AND;只传 productType、不传 orderKind 时不按团属性过滤。前端改动见同日前端 changelog。前端已交付(ab3b6be1):列表与状态计数透传 productType、不再传 orderKind;「订单类型」下拉改核心/定制/团期三页签,团期关键词框只在团期页签显示、切出团期清残留关键词;productTypeTabs/groupBatchKeyword 等 spec 全绿。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 订单服务(order-v3): 订单列表与状态计数新增查询参数 productType
|
||||
|
||||
> **服务**: hl-order-service-v3(端口 8086/8186)
|
||||
> **PR**: #8454(合并提交 `22145977d`)
|
||||
> **Issue**: #8446
|
||||
> **日期**: 2026-09-27
|
||||
> **影响范围**: 管理后台「订单管理v2 › 订单列表」的列表与状态计数接口
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. 列表 `GET /v3/admin/order`(别名 `/v3/admin/order/list`)与状态计数 `GET /v3/admin/order/status-group-counts` 新增查询参数 **`productType`**:`CORE` / `ROUTE` / `CUSTOM` / `GROUP`,英文逗号多值,空串等同不传。
|
||||
2. **只传 `productType`、不传(或传空串)`orderKind` 时,不再缺省 `orderKind=NORMAL`,不按「是否挂团期」过滤。** 页签「团期」因此能查到团期订单。
|
||||
3. 其余组合行为不变:显式传 `orderKind` 与 `productType` 取 AND;传 `groupBatchId` / `groupBatchKeyword` 时仍强制按团期,再与 `productType` 取 AND。两个参数都不传时仍缺省 NORMAL。
|
||||
4. 非法值(如 `productType=XYZ`)返回 HTTP 200 + `code=100001`,与 `orderKind` 非法值同码。
|
||||
5. 响应结构不变。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
wx 2026-09-27 要求订单列表的「订单类型」下拉(散客 / 团单 / 全部)改成「核心 / 定制 / 团期」页签。页签按**产品类型**切分,而原 `orderKind` 按「订单有没有挂运营团期」切分,两者不是同一口径,所以新增 `productType`,`orderKind` 保留原语义不废弃。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 订单列表 | GET | `/v3/admin/order` | 新增参数 | `productType`,别名路径 `/v3/admin/order/list` 同样生效 |
|
||||
| 2 | 订单列表 Tab 分组计数 | GET | `/v3/admin/order/status-group-counts` | 新增参数 | `productType`,与列表同口径 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 订单列表 `GET /v3/admin/order`
|
||||
|
||||
**VO**: `OrderListReqVO` → `Result<PageResult<OrderListItemRespVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
订单列表页签「核心 / 定制 / 团期」切换时,按当前页签传 `productType=CORE|CUSTOM|GROUP` 取列表。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productType | query | String | 否 | `CORE`/`ROUTE`/`CUSTOM`/`GROUP`,多值用英文逗号分隔(如 `CORE,CUSTOM`),空串等同不传 | **新增**。按订单产品类型过滤,与其他条件取 AND |
|
||||
| orderKind | query | String | 否 | `ALL`/`GROUP`/`NORMAL`,空串等同不传 | 原有。未传时缺省 NORMAL;**例外**:传了 productType 而本参数未传或为空串时不缺省、不按团属性过滤 |
|
||||
|
||||
其余参数(`keyword`、`orderStatus`、`statusGroup`、`groupBatchId`、`groupBatchKeyword`、`page`、`pageSize` 等)不变,`pageSize` 上限 100。
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data.records[] | Array | 订单行(前端 `request.js` 拦截器会同步映射为 `list`) |
|
||||
| data.records[].id | String | 订单 ID |
|
||||
| data.records[].productType | String | 产品类型 CORE/ROUTE/CUSTOM/GROUP |
|
||||
| data.total | Number | 符合条件的总条数 |
|
||||
| data.page | Number | 当前页 |
|
||||
| data.pageSize | Number | 每页条数 |
|
||||
|
||||
订单行其余字段不变。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order?productType=CORE&page=1&pageSize=100
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服 2026-09-27 实测,节选前 2 条、每条只保留关键字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 71,
|
||||
"page": 1,
|
||||
"pageSize": 100,
|
||||
"records": [
|
||||
{
|
||||
"id": "2104192144560553985",
|
||||
"orderNo": "HL20260927205115753",
|
||||
"productType": "CORE",
|
||||
"groupBatchId": null,
|
||||
"orderStatus": "CUSTOMIZING"
|
||||
},
|
||||
{
|
||||
"id": "2104161383220457474",
|
||||
"orderNo": "HL20260927184901765",
|
||||
"productType": "CORE",
|
||||
"groupBatchId": null,
|
||||
"orderStatus": "CUSTOMIZING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
没有符合条件的订单时 `records` 为空数组、`total` 为 0,例如 `productType=CORE` 加一个只挂了团期订单的团期关键词。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"records": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
非法值返回 HTTP 200,`code=100001`(测试服实测原文):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100001,
|
||||
"message": "productType 非法,可选值: CORE, ROUTE, CUSTOM, GROUP(多值用逗号分隔)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 多值用英文逗号,命中任一即返回;空串等同不传。
|
||||
- 只传 `productType` 时不按团属性过滤;默认仍排除已取消订单,传 `statusGroup=ALL` 才含已取消(与原口径一致)。
|
||||
- 显式传 `orderKind` 时与 `productType` 取 AND,例如 `productType=GROUP&orderKind=NORMAL` 恒为空。
|
||||
- 传 `groupBatchId` / `groupBatchKeyword` 时强制按团期,再与 `productType` 取 AND;在「核心 / 定制」页签下填团期关键词,只会查到挂了该团期、且产品类型是核心 / 定制的订单。
|
||||
- `ROUTE`(自驾路书)后端接受,本次前端不设页签。
|
||||
- 别名 `/v3/admin/order/list` 入参出参完全相同。
|
||||
|
||||
---
|
||||
|
||||
### 2. 订单列表 Tab 分组计数 `GET /v3/admin/order/status-group-counts`
|
||||
|
||||
**VO**: `OrderListReqVO` → `Result<List<StatusGroupCountVO>>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
页签切换时与列表一起请求,传同一个 `productType`,刷新顶部状态分组徽标。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productType | query | String | 否 | `CORE`/`ROUTE`/`CUSTOM`/`GROUP`,多值用英文逗号分隔,空串等同不传 | **新增**。与列表同口径 |
|
||||
| orderKind | query | String | 否 | `ALL`/`GROUP`/`NORMAL`,空串等同不传 | 原有。与列表同口径,含上面的例外 |
|
||||
|
||||
其余参数与列表相同(不传 `statusGroup` / `orderStatus` / `page` / `pageSize`)。
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data[].group | String | 分组标识,固定 6 项,顺序 ALL、BEFORE_TRIP、ON_TRIP、SETTLEMENT、ABNORMAL、AFTERSALE |
|
||||
| data[].label | String | 分组中文名:全部 / 出行前 / 出行中 / 核单 / 异常/取消 / 售后 |
|
||||
| data[].orderCount | Number | 该分组订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/status-group-counts?productType=GROUP
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
测试服 2026-09-27 实测原文:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{ "group": "ALL", "label": "全部", "orderCount": 14 },
|
||||
{ "group": "BEFORE_TRIP", "label": "出行前", "orderCount": 14 },
|
||||
{ "group": "ON_TRIP", "label": "出行中", "orderCount": 0 },
|
||||
{ "group": "SETTLEMENT", "label": "核单", "orderCount": 0 },
|
||||
{ "group": "ABNORMAL", "label": "异常/取消", "orderCount": 8 },
|
||||
{ "group": "AFTERSALE", "label": "售后", "orderCount": 0 }
|
||||
],
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
没有符合条件的订单时 6 项仍全部返回,`orderCount` 均为 0。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{ "group": "ALL", "label": "全部", "orderCount": 0 },
|
||||
{ "group": "BEFORE_TRIP", "label": "出行前", "orderCount": 0 },
|
||||
{ "group": "ON_TRIP", "label": "出行中", "orderCount": 0 },
|
||||
{ "group": "SETTLEMENT", "label": "核单", "orderCount": 0 },
|
||||
{ "group": "ABNORMAL", "label": "异常/取消", "orderCount": 0 },
|
||||
{ "group": "AFTERSALE", "label": "售后", "orderCount": 0 }
|
||||
],
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
与列表相同,非法值返回 HTTP 200 + `code=100001`(测试服实测原文):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100001,
|
||||
"message": "productType 非法,可选值: CORE, ROUTE, CUSTOM, GROUP(多值用逗号分隔)",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 与列表共用 `OrderListReqVO` 和同一套过滤,页签徽标与列表条数同口径。
|
||||
- 「全部」= 出行前 + 出行中 + 核单,**不含**「异常/取消」,只有显式 `orderKind=ALL` 或 `cancelled=true` 时才计入。只传 `productType` 时不含,与两个参数都不传时一致。上例 14 = 14 + 0 + 0。
|
||||
- 「售后」是横切计数,任何情况下都不计入「全部」。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 页签「核心 / 定制 / 团期」→ 列表与计数都传 `productType=CORE|CUSTOM|GROUP`,**不传 `orderKind`**。
|
||||
- 如果仍带着旧的 `orderKind=NORMAL`,会与 `productType` 取 AND,「团期」页签恒为空。
|
||||
- 旧调用方(只传 `orderKind` 或都不传)行为不变。
|
||||
|
||||
| 组合 | 结果 |
|
||||
|------|------|
|
||||
| `productType=CORE` | 核心产品订单,不看是否挂团期 |
|
||||
| `productType=CORE,CUSTOM` | 核心 + 定制 |
|
||||
| `productType=GROUP&orderKind=NORMAL` | 恒为空 |
|
||||
| 两个都不传 | 缺省 NORMAL(未挂团期的订单),与改动前一致 |
|
||||
| `productType=XYZ` | HTTP 200,`code=100001` |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
无数据库变更,只新增查询条件 `product_type IN (...)`。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
1. `productType` 是独立追加的 AND 条件,不进团属性判断链,所以不会和 `groupBatchId` / `groupBatchKeyword` 互相吞掉(`OrderListScopeFilter.java:44`、`:54-55`)。
|
||||
2. 只传 `productType` 时 Service 把 `orderKind` 置 null 而不是 `ALL`:置 `ALL` 会让「全部」徽标计入已取消(`OrderService.java:529`)。
|
||||
3. `ROUTE` 测试服当前产品与订单都是 0 条,后端已支持,将来加页签无需改后端。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### productType(产品类型)
|
||||
|
||||
**所属字段**: 入参 `OrderListReqVO.productType`、出参 `records[].productType` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 页签 |
|
||||
|----|------|------|
|
||||
| `CORE` | 核心 | 核心 |
|
||||
| `CUSTOM` | 定制 | 定制 |
|
||||
| `GROUP` | 团期 | 团期 |
|
||||
| `ROUTE` | 自驾路书 | 本次不设 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 入参 `productType` | 无 | 新增,多值 |
|
||||
| 只传 `productType`、不传 `orderKind` | —(参数不存在) | 不缺省 NORMAL,不按团属性过滤 |
|
||||
| 只传 `orderKind` 或都不传 | 都不传时缺省 NORMAL | 不变 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **向后兼容**:是。不传 `productType` 的调用方行为不变。
|
||||
- **前端是否必须同步**:前端改页签时列表与计数都要改传 `productType`、删掉 `orderKind`;不改前端时现有下拉照常可用。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 订单详情、创建、编辑等其他接口不变。
|
||||
- 定制师待办列表(`DesignerOrderListService`)、财务订单查询(`FinanceOrderQueryApiImpl`)复用同一查询对象但不设 `productTypes`,行为不变。
|
||||
- 小程序(`MpOrderListReqVO`)、H5 不涉及。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
测试服 order-v3 `22145977d`,经网关、测试专用 ADMIN 账号,逐条与同条件 SQL(`hl_order_service_v3.order_main`,`deleted_at IS NULL`,默认排除已取消)比对,全部一致:
|
||||
|
||||
| 请求 | total / 计数 | SQL |
|
||||
|------|------|------|
|
||||
| `productType=CORE` / `CUSTOM` / `GROUP` | 71 / 2 / 14,逐条 productType 全匹配 | 71 / 2 / 14 |
|
||||
| 同上 + `statusGroup=ALL` | 86 / 2 / 22 | 86 / 2 / 22 |
|
||||
| `/v3/admin/order/list?productType=GROUP` | 14 | 与主路径相同 |
|
||||
| `productType=CORE,CUSTOM` | 73 | 71 + 2 |
|
||||
| `productType=`(空串)/ 都不传 | 73 / 73 | NORMAL 口径 73 |
|
||||
| `orderKind=GROUP` / `orderKind=ALL` | 14 / 87 | 14 / 87 |
|
||||
| 计数 `productType=GROUP` | 全部 14、出行前 14、出行中 0、核单 0、异常/取消 8、售后 0 | 逐组一致 |
|
||||
| 计数不传 productType | 全部 73、出行前 43、出行中 0、核单 30、异常/取消 15、售后 0 | 与上一行不同,参数生效 |
|
||||
| 团期关键词 / + `productType=GROUP` / + `productType=CORE` | 12 / 12 / 0 | 该团期 CORE 订单 0 条 |
|
||||
| `productType=XYZ`(列表、计数) | HTTP 200,`code=100001` | — |
|
||||
|
||||
单测:OrderServiceTest 323、OrderListScopeFilterTest 18、OrderListReqVOValidationTest 16、OrderGlobalExceptionHandlerTest 9、OrderControllerTest 56、OrderInfoMapperIT 30(Testcontainers 真库),全绿。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 接口文档:`docs/order-v3/api/API-SPEC.html`(订单列表参数表、orderKind 例外、§1.11 计数口径)
|
||||
- 入参:`hl-order-service-v3/src/main/java/com/hulalv/order/core/controller/admin/vo/OrderListReqVO.java:91-104`(productType)、`:59-63`(orderKind 例外说明)
|
||||
- 过滤:`hl-order-service-v3/src/main/java/com/hulalv/order/core/mapper/OrderListScopeFilter.java:54-55`
|
||||
- 错误码:`hl-order-service-v3/src/main/java/com/hulalv/shared/exception/OrderGlobalExceptionHandler.java:104-110`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8446](https://git.1814.love/wx/HL/issues/8446)
|
||||
- **PR**: [#8454](https://git.1814.love/wx/HL/pulls/8454)
|
||||
- **前端 changelog**: `27_frontend_订单列表订单类型改页签-前端优化-管理后台.md`
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "房务订单列表户级表格补团号展示"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "e3f8de237672691e6fc62d0ac7de3c747c733d14"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-27"
|
||||
status_note: "后端接口 GET /v3/admin/order/house-allocation/households 已返回团号字段 teamNo,前端户级表格 src/views/housekeeper/orders/HouseholdTable.vue 需补展示。2026-09-27 测试服实测:scope=all 全部状态第 1 页 100 行(total=104)都含 teamNo 键,55 行有值(如 26-8972),其余为 null;null 一律显示 -。前端已交付(mmg 2026-09-27,ref e3f8de23):HouseholdTable 订单号后加独立团号列 width 100,teamNo||'-' mono 直显,scroll-x 1110→1210;spec 8 例全绿,已验证。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 房务订单列表:户级表格补团号展示
|
||||
|
||||
> **页面**: 房务管家 › 订单列表(路由 `housekeeper/orders`)
|
||||
> **表格文件**: `src/views/housekeeper/orders/HouseholdTable.vue`
|
||||
|
||||
## 关键变化
|
||||
|
||||
房务订单列表户级表格当前展示 9 列(订单号/客人/产品/出发日期/配房状态/认领人/定制师/订单金额/操作),**缺少团号**。数据已就绪:接口 `GET /v3/admin/order/house-allocation/households` 每行都带 `teamNo` 字段,前端只需加列展示。
|
||||
|
||||
| 现象 | 根因 | 后端已给什么 |
|
||||
|---|---|---|
|
||||
| 户级表格没有团号 | 前端表格列定义未包含 `teamNo` | 所有行都带 `teamNo` 字段(实测第 1 页 100 行:55 行有值,其余为 null) |
|
||||
|
||||
## 接口与字段
|
||||
|
||||
**GET** `/v3/admin/order/house-allocation/households`
|
||||
|
||||
**响应体中的团号字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `teamNo` | String / null | 团号;可为 null,显示 `-` |
|
||||
|
||||
- 团号在订单首次付款成功时生成(线上订金支付或线下收款登记首付款);在那之前 `teamNo` 为 null,属正常状态,显示 `-`,不提示、不报错。
|
||||
- 主订单被软删的存量行,`orderNo` 与 `teamNo` 都为 null,同样用 `-` 兜底。
|
||||
|
||||
## 前端需要做什么
|
||||
|
||||
在 `src/views/housekeeper/orders/HouseholdTable.vue` 中:
|
||||
|
||||
1. 在表格列定义(约 182~216 行)**加入团号列**,具体位置由前端定(可单独一列,或放在订单号下方作副行)。
|
||||
2. 渲染规则:`teamNo` 为 null 或空时显示 `-`,否则显示该值。
|
||||
3. 关键词搜索已支持按团号模糊匹配(keyword 对订单号/团号/客人/产品名做 OR),无需改搜索逻辑。
|
||||
|
||||
## 验证方式
|
||||
|
||||
改完后在测试服房务管家订单列表逐条核对:
|
||||
|
||||
1. 户级表格新增团号列,显示正确。
|
||||
2. 有团号的行显示具体值(如 `26-8972`),`teamNo` 为 null 的行显示 `-`。
|
||||
- 测试服上有一批「已付款」测试订单的 `teamNo` 也是 null:它们是经订单状态干预接口造的测试数据,该接口推进到已付款时不生成团号(后端缺口,已建单 wx/HL#8408)。这些行显示 `-` 就是正确结果,前端不需要特殊处理。
|
||||
3. 按团号搜索能正确筛选。
|
||||
|
||||
## 不影响范围
|
||||
|
||||
- 后端接口零改动。
|
||||
- 「团期」页签(`GroupBatchTable.vue`)展示的是团期号 `batchNo`,与本条无关。
|
||||
- 团期详情页不涉及。
|
||||
- 小程序、H5 不涉及。
|
||||
|
||||
## 附:数据完整性验证
|
||||
|
||||
2026-09-27 测试服(管理后台房务管家订单列表,scope=all 全部状态首页 100 行):
|
||||
|
||||
- **100 行都带 `teamNo` 键** ✓
|
||||
- **55 行有值**(形如 `26-8972`)✓
|
||||
- 其余 45 行为 null(其中 2 行是主订单软删的存量行,`orderNo` 也为 null)
|
||||
|
||||
---
|
||||
|
||||
## 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "frontend"
|
||||
title: "房务订单列表「类型」改页签 + 「小蒙马」改「团期」文案 + 菜单改名后页面名同步"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "not_required"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ce469f7830a50053e3070f379b05fb075e6cd9ef"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-28"
|
||||
status_note: "后端菜单改名与字典改名已在测试服生效。前端三项改动互不依赖:房务订单列表「类型」改页签、写死的「小蒙马」改「团期」、出团管理/出团详情页面名同步。2026-09-27 订正:写死文案 5 个文件 8 处、菜单名同步补「返回班期看板」按钮与 spec 路径、页签绑定改 @update:value。前端已交付(ce469f78,与 28_frontend 同一交付):「类型」下拉改页签(28 口径两页签);「小蒙马」改「团期」5 文件 8 处写死文案;菜单改名同步(班期看板→出团管理、团期详情→出团详情等)及两 spec 断言;房务 11 例+菜单两 spec 38 例全绿。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 前端优化:房务订单列表与全局文案同步(第二期菜单与业务定案对齐)
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
三项联动改动,无依赖关系,可独立完成但应同步推送:
|
||||
|
||||
1. **房务管家 › 订单列表:「类型」下拉改为页签**
|
||||
- 改为三页签(核心 / 定制 / 团期),去掉「全部」选项,默认「核心」
|
||||
- 样式照「财务管理 › 核单列表」(`src/views/finance/settlement/index.vue:58-66`)的「常规产品 / 团期产品」页签
|
||||
- 端点不变:核心、定制走户级端点 `house-allocation/households`,团期走 `house-allocation/group-batches`
|
||||
|
||||
2. **全局文案替换:「小蒙马」→「团期」**
|
||||
- 后端字典已改,走字典翻译的地方自动生效
|
||||
- 写死的用户可见文案共 5 个文件 8 处需手改(见第三节表格)
|
||||
|
||||
3. **菜单改名后的前端页面名同步**
|
||||
- 后端菜单改名,侧栏自动生效
|
||||
- 前端写死的页面标题、按钮与提示文案共 4 个文件 4 处,另有 2 个 spec 断言跟着改(见第四节表格)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
wx 2026-09-27 确认三项协同改动,后端部分(菜单改名、字典值改动)已在测试服生效(PR #8450,#8444)。前端需消费这些变化以保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 二、订单列表「类型」改为页签
|
||||
|
||||
> 🔴 **本章的页签集合已被 2026-09-28 的口径取代,实施前先看新条目。**
|
||||
> 新条目:`changelogs-v2/2026-09/28_frontend_房务订单列表改常规与团期两页签-前端优化-管理后台.md`
|
||||
>
|
||||
> wx 2026-09-28 把页签定为**两个**:「常规订单 / 团期订单」。与本章的差别只有两处——
|
||||
> ①页签由三个(核心/定制/团期)改为两个;②本章「去掉『全部』、`orderTab` 初值改成 `ref('CORE')`」**作废**,
|
||||
> 「全部」保留并改名为「常规订单」,初值保持 `ref('all')` 不动。
|
||||
>
|
||||
> **本章其余要点全部继续有效**:页签置于 ProTable toolbar 左侧、绑定用 `:value` + `@update:value`
|
||||
> (不能换成 `v-model:value`)、端点行为不变、`?groupBatchId=` 自动切团期页签、注释同步。
|
||||
> 第三章(「小蒙马」→「团期」文案替换)与第四章(菜单改名后页面名同步)**完全不受影响,照原样执行**。
|
||||
|
||||
### 现状(ref `gitea/v2.1` origin/v2.1)
|
||||
|
||||
**文件**:`src/views/housekeeper/orders/index.vue`
|
||||
|
||||
- **搜索区** `:34-45`:「类型」使用 `n-select` 下拉框,宽度 120px,`:value="orderTab"` + `@update:value="onOrderTabChange"`。`:34` 的注释写明它是 2026-09-27 当天由页签改成下拉的,本次按 wx 要求改回页签
|
||||
- **状态变量** `:365`:`const orderTab = ref('all')`
|
||||
- **选项定义** `:366-372`:
|
||||
```js
|
||||
const orderTabOptions = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'CORE', label: '核心' },
|
||||
{ value: 'CUSTOM', label: '定制' },
|
||||
{ value: 'GROUP', label: '团期' },
|
||||
]
|
||||
```
|
||||
|
||||
### 改动要点
|
||||
|
||||
#### 1. 样式改为页签,置于 ProTable toolbar 左侧
|
||||
- 参考文件:`src/views/finance/settlement/index.vue:58-66`
|
||||
- 结构:`<n-flex justify="space-between" class="table-summary">` 左侧放页签
|
||||
- 页签配置:`<n-tabs type="line" size="small" class="product-tabs">`
|
||||
- 样式(同文件 `:217` 附近):
|
||||
```less
|
||||
.product-tabs {
|
||||
width: 280px;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 页签内容:**去掉「全部」,仅保留核心、定制、团期,默认核心**
|
||||
```vue
|
||||
<n-tabs
|
||||
:value="orderTab"
|
||||
type="line"
|
||||
size="small"
|
||||
class="product-tabs"
|
||||
@update:value="onOrderTabChange"
|
||||
>
|
||||
<n-tab name="CORE">核心</n-tab>
|
||||
<n-tab name="CUSTOM">定制</n-tab>
|
||||
<n-tab name="GROUP">团期</n-tab>
|
||||
</n-tabs>
|
||||
```
|
||||
- 绑定方式与现有 `n-select` 相同:`:value="orderTab"` + `@update:value="onOrderTabChange"`,仍由现有回调重置状态桶。核单列表用的是 `v-model:value`,这里不能照抄,否则切换时会绕过 `onOrderTabChange`
|
||||
- `:365` 的初始值由 `ref('all')` 改为 `ref('CORE')`;`orderTabOptions` 删除;凡把 `orderTab` 置为或判断为 `'all'` 的地方(如 `:456` 的 `orderTab.value !== 'all'`)按「默认 CORE、恒有值」改。同文件 `scope`、`statusTab` 也有 `'all'`,与本改动无关,不动
|
||||
|
||||
#### 3. 端点行为不变
|
||||
- 核心、定制:`GET house-allocation/households`,带 `productType=CORE|CUSTOM`
|
||||
- 团期:`GET house-allocation/group-batches`(整团一行)
|
||||
- 切换页签重置状态桶(现有 `onOrderTabChange` 逻辑)
|
||||
|
||||
#### 4. 通知链接行为
|
||||
- `?groupBatchId=` 仍自动切到「团期」页签并精确定位
|
||||
- `?orderId=` 行为不变
|
||||
|
||||
#### 5. 页面注释同步
|
||||
- `:34-35`、`:297-308`、`:366` 的注释里「下拉 / 全部」的描述同步改成「页签 / 核心·定制·团期」
|
||||
|
||||
---
|
||||
|
||||
## 三、全局文案替换:「小蒙马」→「团期」
|
||||
|
||||
### 后端字典已改
|
||||
后端 PR #8450(HL-#8444)部署完毕,字典值修改:
|
||||
- `product_type/GROUP` label 为「团期」(原「小蒙马」)
|
||||
- `vehicle_type/GROUP` label 为「团期专用」(原「小蒙马专用」)
|
||||
- 走字典翻译的地方自动生效
|
||||
|
||||
### 前端需改的写死文案(5 个文件 8 处,均对 origin/v2.1 `1c555f5` 逐行核过)
|
||||
|
||||
| 文件 | 行号 | 改前 | 改后 |
|
||||
|------|------|------|------|
|
||||
| `src/views/grassland-guide/ProductPickerModal.vue` | 26 | placeholder="核心产品 / 小蒙马" | placeholder="核心产品 / 团期" |
|
||||
| `src/views/grassland-guide/ProductPickerModal.vue` | 132 | { label: '小蒙马', value: 'GROUP' } | { label: '团期', value: 'GROUP' } |
|
||||
| `src/views/housekeeper/_shared/constants.js` | 68 | GROUP: '小蒙马' | GROUP: '团期' |
|
||||
| `src/views/housekeeper/_shared/constants.js` | 71 | xmm: '小蒙马' | xmm: '团期' |
|
||||
| `src/views/order-v2/new/components/Step0Theme.vue` | 252 | GROUP: { label: '小蒙马', cls: ... | GROUP: { label: '团期', cls: ... |
|
||||
| `src/views/order-v2/new/components/Step0Theme.vue` | 258 | { key: 'GROUP', label: '小蒙马' } | { key: 'GROUP', label: '团期' } |
|
||||
| `src/views/product/batch/BatchEditModal.vue` | 195 | '小蒙马产品下单时需要...' | '团期产品下单时需要...' |
|
||||
| `src/views/product/edit/components/steps/BatchPricingStep.vue` | 348 | `<p class="bps-desc">小蒙马按班期招募...` | `<p class="bps-desc">团期按班期招募...` |
|
||||
|
||||
`constants.js:71` 的 `xmm` 是旧产品类型键(同文件 `PRODUCT_TYPE.XMM = 'xmm'`),只改 label,键名不动。
|
||||
|
||||
### 注释(顺手改,不影响展示)
|
||||
- `src/views/order-v2/new/components/Step0Theme.vue:249`:`CORE 核心产品 / GROUP 小蒙马 / CUSTOM 私人定制` → `GROUP 团期`
|
||||
- 其余注释命中:`src/constants/productType.js:14,33`、`src/stores/productEdit.js:139`、`housekeeper/components/orderDetailAdapter.js:507`、`housekeeper/ledger/index.vue:183`、`housekeeper/todos/index.vue:273`、`product/edit/` 下 `DayEditor.vue:39`、`HotelCard.vue:12`、`HotelPickerModal.vue:36,308`、`config/product-type-config.js:2,5`、`index.vue:3`、`product/line/components/MiniappShareModal.vue:189`
|
||||
|
||||
### 不在本次替换范围
|
||||
- `src/views/product/edit/config/block-type-config.js:97,101,129,307,311,313`:产品详情页营销模块的默认文案(「小蒙马领队团队」「今年,小蒙马 2 岁了」「小蒙马亲子营」)。这里的「小蒙马」是品牌名,不是产品类型名,替换成「团期」语义不通,保持原样。
|
||||
|
||||
---
|
||||
|
||||
## 四、菜单改名后的前端页面名同步
|
||||
|
||||
后端已改名的菜单及对应前端页面标题:
|
||||
|
||||
| 菜单变化 | 前端文件 | 改前 | 改后 |
|
||||
|---------|---------|------|------|
|
||||
| 订单管理v2 › 团期订单 → 出团管理 | `src/views/order-v2/batch/index.vue:3` | title="班期看板" | title="出团管理" |
|
||||
| 隐藏页:团期详情 → 出团详情 | `src/views/order-v2/batch/detail/index.vue:3` | title="团期详情" | title="出团详情" |
|
||||
| 同上(详情页空态按钮) | `src/views/order-v2/batch/detail/index.vue:348` | action-text="返回班期看板" | action-text="返回出团管理" |
|
||||
| 同上(订单详情里的引导提示) | `src/views/order-v2/detail/_shared/orderDetailActions.js:32` | '…请到团期详情完成团期确认后…' | '…请到出团详情完成团期确认后…' |
|
||||
| 同上(测试断言) | `src/views/order-v2/detail/_shared/__tests__/orderDetailActions.spec.js:73` | toContain('团期详情') | toContain('出团详情') |
|
||||
| 同上(测试断言) | `src/views/order-v2/detail/components/__tests__/BottomActionBar.spec.js:73` | toContain('团期详情') | toContain('出团详情') |
|
||||
| 车务管理 › 团期配车 › 团期配车 → 车务管理 › 团期配车(拍平) | `src/views/fleet/group-dispatch/index.vue` | —— | **无需改** |
|
||||
|
||||
#### 特别说明
|
||||
- 「班期看板」页面标题改为「出团管理」,与侧栏菜单同名
|
||||
- 「团期详情」页面标题改为「出团详情」,引导提示与测试断言一起改。后端 581065 的 message 是「团期单随团期确认,不可单独确认」,不含页面名,前端改提示不会和后端文案对不上
|
||||
- 团期配车页面路由与菜单 path 都没变,只是菜单少了一层目录,前端无需改动
|
||||
- 代码注释里的「团期详情」(`src/api/`、`stores/`、`notification/` 等处)指的是接口或数据,不是页面名,不用改
|
||||
|
||||
---
|
||||
|
||||
## 五、验证检查清单
|
||||
|
||||
改完后逐项核对(无需部署,仅检查代码变更):
|
||||
|
||||
- [ ] 房务订单列表的「类型」改为三页签(核心 / 定制 / 团期),去掉「全部」
|
||||
- [ ] 页签默认「核心」,样式同「财务管理 › 核单列表」;切换页签时状态桶照常重置
|
||||
- [ ] `?groupBatchId=` 通知链接仍自动切到「团期」页签并定位
|
||||
- [ ] 5 个文件 8 处写死文案「小蒙马」改「团期」
|
||||
- [ ] 「班期看板」页面标题 →「出团管理」,详情页空态按钮「返回班期看板」→「返回出团管理」
|
||||
- [ ] 「团期详情」页面标题 →「出团详情」
|
||||
- [ ] 订单详情里的团期确认引导提示改「出团详情」
|
||||
- [ ] `orderDetailActions.spec.js`、`BottomActionBar.spec.js` 断言同步,单测绿
|
||||
|
||||
---
|
||||
|
||||
## 六、不影响范围
|
||||
|
||||
- 后端接口零改动
|
||||
- 小程序、H5 不涉及
|
||||
- 团期详情页除标题外的其它区域不涉及
|
||||
- 房务配房列表的其它筛选条件不涉及
|
||||
|
||||
---
|
||||
|
||||
## 七、相关工单与文档
|
||||
|
||||
- 后端菜单改名:PR #8450(HL-#8444),已部署测试服
|
||||
- 字典值改动:同上 PR
|
||||
- 前端对接无新的接口契约变更
|
||||
|
||||
---
|
||||
|
||||
## 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8446"
|
||||
title: "订单列表「订单类型」下拉改为「核心 / 定制 / 团期」页签"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "前端优化"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "verified"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "ab3b6be1268721e56ed809da9329b367d8b890e9"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-28"
|
||||
status_note: "后端查询参数 productType 已部署测试服并实测(#8446,PR #8454)。前端:订单列表「订单类型」下拉改为「核心 / 定制 / 团期」页签,默认核心;列表与状态计数请求传 productType,不再传 orderKind。与 27_8446 接口件同一交付(ab3b6be1):页签落 ProTable 工具条,:value+@update:value 触发列表+计数同刷(不用 v-model);默认核心页签。"
|
||||
updated_at: "2026-09-27"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 前端优化:订单列表「订单类型」下拉改为「核心 / 定制 / 团期」页签
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **订单管理v2 › 订单列表**:删除搜索区「订单类型」下拉(散客 / 团单 / 全部,默认散客),改为表格工具栏左侧的页签「核心 / 定制 / 团期」,默认「核心」。样式同「财务管理 › 核单列表」的「常规产品 / 团期产品」页签。
|
||||
2. **列表请求与状态计数请求都传当前页签的 `productType=CORE|CUSTOM|GROUP`,不再传 `orderKind`。**
|
||||
- 限定:如果请求里还带着 `orderKind=NORMAL`,后端会与 `productType` 取 AND,「团期」页签恒为空(团期订单都挂了团期)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
wx 2026-09-27:「把订单列表的页面的类型也换成tab 也是 核心/定制/团期」。页签按订单**产品类型**切分,与同日房务管家订单列表的页签口径一致。原「散客 / 团单」按「订单有没有挂团期」切分,和产品类型不是一回事。
|
||||
|
||||
后端新增查询参数 `productType`(#8446,PR #8454),契约详见同日接口 changelog `27_8446_订单列表按产品类型筛选-修改接口-管理后台.md`。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状(hl-ui `origin/v2.1` @ `1c555f5`,逐行核过)
|
||||
|
||||
文件 `src/views/order-v2/list/index.vue`:
|
||||
|
||||
| 位置 | 现状 |
|
||||
|------|------|
|
||||
| `:55-64` | 搜索区「订单类型」`n-select`:`v-model:value="searchForm.orderKind"`、`:options="orderKindOptions"`、`@update:value="handleSearch"` |
|
||||
| `:139` | `import { …, ORDER_KIND_OPTIONS } from '@/api/orderV2'` |
|
||||
| `:234-235` | `const orderKindOptions = ORDER_KIND_OPTIONS` 及 #7635 注释 |
|
||||
| `:236-255` | `adaptedApi.getPage`:`searchForm` 除日期 / 定制师 / 标签外的字段经 `...rest` 原样透传给 `getOrderPage` |
|
||||
| `:279-280` | `useListPage` 的 `searchFields`:`{ key: 'orderKind', default: 'NORMAL' }` |
|
||||
| `:306-308`、`:322-323` | `fetchGroupCounts` 手工拼参数:`if (searchForm.orderKind) params.orderKind = searchForm.orderKind`,以及描述入参的注释 |
|
||||
| `:344-347` | `handleSearch()`:`doSearch()` 刷列表 + `fetchGroupCounts()` 刷状态计数 |
|
||||
| `:103-120` | ProTable `#toolbar`:左「共 N 条」,右「新增订单」按钮 |
|
||||
|
||||
`src/api/orderV2.js`:`:25-33` 定义 `ORDER_KIND` / `ORDER_KIND_OPTIONS`;`getOrderPage`(`:114`)与 `getOrderStatusGroupCounts`(`:137`)的 jsdoc 列了 `orderKind`(`:108`、`:133`)。
|
||||
|
||||
---
|
||||
|
||||
## 三、改动要点
|
||||
|
||||
1. **删除下拉**:删掉 `index.vue:55-64` 整个「订单类型」`form-item`、`:234-235` 的 `orderKindOptions`、`:139` 的 `ORDER_KIND_OPTIONS` import。
|
||||
2. **搜索字段**:`:279-280` 的 `{ key: 'orderKind', default: 'NORMAL' }` 改为 `{ key: 'productType', default: 'CORE' }`。列表请求经 `...rest` 自动带上 `productType`,`adaptedApi.getPage` 不用改。
|
||||
3. **状态计数**:`:322-323` 改为 `if (searchForm.productType) params.productType = searchForm.productType`,`:306-308` 注释里的 `orderKind` 同步改成 `productType`。
|
||||
4. **页签**:放在 ProTable `#toolbar` 左侧,写法照核单列表 `src/views/finance/settlement/index.vue:52-66`:外层 `<n-flex justify="space-between" align="center" class="table-summary">`,左边页签,右边放现有的「共 N 条」和「新增订单」按钮。样式照该文件 `:213-219`:`.table-summary { width: 100%; }`、`.product-tabs { width: 280px; }`。
|
||||
|
||||
```vue
|
||||
<n-tabs
|
||||
:value="searchForm.productType"
|
||||
type="line"
|
||||
size="small"
|
||||
class="product-tabs"
|
||||
@update:value="onProductTypeChange"
|
||||
>
|
||||
<n-tab name="CORE">核心</n-tab>
|
||||
<n-tab name="CUSTOM">定制</n-tab>
|
||||
<n-tab name="GROUP">团期</n-tab>
|
||||
</n-tabs>
|
||||
```
|
||||
|
||||
```js
|
||||
// 切页签 = 原下拉的 @update:value="handleSearch":列表与状态计数一起刷新
|
||||
function onProductTypeChange(v) {
|
||||
searchForm.productType = v
|
||||
handleSearch()
|
||||
}
|
||||
```
|
||||
|
||||
核单列表用的是 `v-model:value`,切换时不发请求,这里不能照抄。
|
||||
5. **重置**:「重置」走 `doReset`,按 `searchFields` 默认值把页签恢复为「核心」,与原下拉重置回「散客」同构,不用额外处理。
|
||||
6. **`src/api/orderV2.js`**:`getOrderPage` 与 `getOrderStatusGroupCounts` 的 jsdoc 补 `productType`。`ORDER_KIND` / `ORDER_KIND_OPTIONS` 列表页不再使用;如果删除,同步删 `src/api/__tests__/orderV2.spec.js:444-448` 的断言,保留也不影响功能。
|
||||
|
||||
---
|
||||
|
||||
## 四、调用时会撞上的限定
|
||||
|
||||
- `productType` 取 `CORE` / `CUSTOM` / `GROUP`。后端还接受 `ROUTE`(自驾路书),本次不设页签。非法值返回 HTTP 200 + `code=100001`。
|
||||
- 不传 `orderKind` 时,后端不再按「是否挂团期」过滤,只按产品类型分。状态计数「全部」的取消口径与不传 `productType` 时一致。
|
||||
- **「团期」关键词输入框**(`groupBatchKeyword`,`index.vue:67-76`):填了关键词时,后端只查挂了命中团期的订单,再与 `productType` 取 AND。在「核心 / 定制」页签下填团期关键词,结果基本为空(测试服当前没有「核心产品却挂了团期」的订单)。建议该输入框只在「团期」页签显示,由前端决定。
|
||||
- 响应结构不变:列表后端返回 `{ records, total, page, pageSize }`,`src/utils/request.js:445` 拦截器已映射为 `list`,前端照旧读 `res.list`;状态计数 `[{ group, label, orderCount }]` 固定 6 项。
|
||||
|
||||
---
|
||||
|
||||
## 五、验证清单(前端改完自查)
|
||||
|
||||
- [ ] 订单列表不再有「订单类型」下拉,工具栏左侧出现「核心 / 定制 / 团期」页签,默认「核心」
|
||||
- [ ] 切换页签时,列表与顶部状态分组计数一起刷新;两个请求的 query 都带 `productType`,不带 `orderKind`
|
||||
- [ ] 「团期」页签有数据;「核心」页签不再混入团期订单
|
||||
- [ ] 点「重置」回到「核心」
|
||||
- [ ] 若删了 `ORDER_KIND_OPTIONS`,`orderV2.spec.js` 断言同步删除,单测绿
|
||||
|
||||
---
|
||||
|
||||
## 六、不影响范围
|
||||
|
||||
- 订单详情、出团管理、房务管家订单列表不涉及
|
||||
- 小程序、H5 不涉及
|
||||
|
||||
---
|
||||
|
||||
## 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8384"
|
||||
title: "团期预支防双花:团单子订单禁走订单级预支入口(新错误码 589558)+ 团期财务在途预支口径补 PAID"
|
||||
consumer: "admin"
|
||||
author: "yst"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "577683cf59a5ce3bf74bc48e6682be745de638c1"
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-29"
|
||||
status_note: "前端已交付(团期子订单隐藏订单级预支按钮,589558 拦截器透 message 兜底),详见 hl-admin v2.1 提交 577683cf。"
|
||||
updated_at: "2026-09-28"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 【修改接口·管理后台】团期预支防双花 (#8384)
|
||||
|
||||
> **PR**: #8479 | **服务**: hl-order-service-v3 | **更新时间**: 2026-09-28 16:30
|
||||
|
||||
## 1. 接口背景
|
||||
|
||||
团期预支走「统一池」口径:同一团期下所有预支(团期级 + 各子订单级)共用一个尾款池、共享一条额度上限。本次修复该模型下的两个「双花」缺口:
|
||||
|
||||
1. **入口双花**:团期产品的子订单(即订单挂在某个团期下的单)此前仍可以从「订单级预支」入口申请预支,同一笔尾款可能在订单级、团期级两个入口被重复支取。业务拍板:**团期子订单只允许走团期级预支入口,订单级入口一律拒绝**。
|
||||
2. **口径双花**:团期财务 Tab 的「可支取余额」在计算扣减项时,漏算了「已付款但核单尚未扣回」的预支(状态 PAID),导致这笔钱在核单完成前被重复放出额度。本次把 PAID 补入扣减口径,与订单级预支上限的既有口径对齐。
|
||||
|
||||
## 2. 变更清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 创建预支(订单级) | POST | `/v3/admin/order/{orderId}/advance` | 行为变更 | 团期子订单调用直接返回新错误码 **589558**,不再创建预支;普通散客订单行为不变 |
|
||||
| 2 | 团期财务总览(财务 Tab) | GET | `/v3/admin/order/group-batch/{groupBatchId}/finance` | 行为变更 | 「可支取余额」的统计口径补入 PAID 状态预支;字段结构零变化,返回值数值可能变小 |
|
||||
|
||||
## 3. 接口详情
|
||||
|
||||
### 3.1 创建预支(订单级)
|
||||
|
||||
- **使用场景**:订单详情页为单个订单的报账人申请预支借款(创建即进待审批)
|
||||
- **认证**:需管理后台 JWT
|
||||
- **幂等性**:否(同单并发创建由后端串行化)
|
||||
- **限流**:无
|
||||
|
||||
### 3.2 团期财务总览(财务 Tab)
|
||||
|
||||
- **使用场景**:团期详情页「财务」Tab,展示整团应收/已收/待收/预支与逐户明细
|
||||
- **认证**:需管理后台 JWT + `group-batch:finance:view` 权限
|
||||
- **幂等性**:是(只读查询)
|
||||
- **限流**:无
|
||||
|
||||
## 4. 接口入参
|
||||
|
||||
### 4.1 创建预支(POST `/v3/admin/order/{orderId}/advance`)
|
||||
|
||||
#### 路径参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| orderId | Long | 必填 | 订单 ID |
|
||||
|
||||
#### 请求体字段
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||
|------|------|------|------|----------|
|
||||
| payeeStaffId | Long | 必填 | 借款对象(取借款对象候选下拉接口 `GET /v3/admin/order/{orderId}/advance/payee-candidates` 返回的 id) | 必须为本订单的在职人员 |
|
||||
| advanceType | String | 必填 | 预支借款类型(取数据字典 `advance_type` 的 dictValue) | 非空 |
|
||||
| amount | Number | 必填 | 预支金额 | 必须 > 0,且不超过可支取余额上限 |
|
||||
| purpose | String | 可选 | 用途说明 | — |
|
||||
| voucherUrl | String | 可选 | 凭证文件 URL | — |
|
||||
|
||||
### 4.2 团期财务总览(GET `/v3/admin/order/group-batch/{groupBatchId}/finance`)
|
||||
|
||||
#### 路径参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| groupBatchId | Long | 必填 | 团期 ID |
|
||||
|
||||
无请求体、无 Query 参数。
|
||||
|
||||
## 5. 出参(响应)
|
||||
|
||||
### 5.1 创建预支 → `OrderAdvanceRespVO`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | String(Long) | 预支 ID |
|
||||
| orderId | String(Long) | 订单 ID |
|
||||
| teamNo | String | 团号 |
|
||||
| payeeStaffId | String(Long) | 报账人 staff 分配 ID(快照) |
|
||||
| payeeName | String | 报账人姓名(快照) |
|
||||
| payeeRole | String | 报账人角色代码 |
|
||||
| payeeRoleText | String | 报账人角色中文文案 |
|
||||
| advanceType | String | 预支类型 |
|
||||
| amount | Number | 预支金额 |
|
||||
| purpose | String | 用途说明 |
|
||||
| voucherUrl | String | 凭证文件 URL |
|
||||
| status | String | 状态代码(枚举见 §6) |
|
||||
| statusText | String | 状态中文文案 |
|
||||
| rejectReason | String | 驳回原因(仅 REJECTED 有值) |
|
||||
| createdByName | String | 创建人姓名 |
|
||||
| createTime | String(日期时间) | 创建时间 |
|
||||
| submittedAt | String(日期时间) | 提交审批时间 |
|
||||
| approvedAt | String(日期时间) | 审批时间 |
|
||||
| approvedBy | String | 审批人姓名 |
|
||||
|
||||
### 5.2 团期财务总览 → `GroupBatchFinanceRespVO`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| receivableAmount | String(金额) | 整团应收 |
|
||||
| receivedAmount | String(金额) | 整团已收 |
|
||||
| unpaidAmount | String(金额) | 整团待收(本期总尾款,预支上限计算基数) |
|
||||
| advanceApproved | String(金额) | 已预支 = 本团期 APPROVED 状态预支合计(团期级 + 各子订单级) |
|
||||
| advancePending | String(金额) | 待审批预支 = 本团期 SUBMITTED 状态预支合计 |
|
||||
| advanceAvailable | String(金额) | **可支取余额** = 团期尾款池 −(待审批 + 已通过 + 已付款未核单),下限 0。**本次口径变化字段** |
|
||||
| items | Array | 逐户付款明细(一户一行,全量返回不分页) |
|
||||
| totals | Object | 整团合计行(totalPrice / paidAmount / unpaidAmount) |
|
||||
| withdrawnCount | Integer | 已退团户数 |
|
||||
| primaryPayeeName | String | 主报账人姓名(未设置为 null) |
|
||||
| secondaryPayeeName | String | 次报账人姓名(未设置为 null) |
|
||||
|
||||
> 字段结构**零变化**;变化的是 `advanceAvailable` 的计算口径(见 §10)。
|
||||
|
||||
## 6. 枚举 / 数据字典
|
||||
|
||||
### 6.1 预支状态 status(`AdvanceStatus`)
|
||||
|
||||
**所属字段**:`OrderAdvanceRespVO.status` | **类型**:`String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `SUBMITTED` | 待审批 | 创建即进此态,占额度 |
|
||||
| `APPROVED` | 已通过 | 财务审批通过,占额度 |
|
||||
| `PAID` | 已支付 | 出纳已付款、核单未扣回,**仍占额度**(核单完成才释放) |
|
||||
| `REJECTED` | 已驳回 | 终态,不占额度 |
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
| code | 含义 | 触发场景 |
|
||||
|------|------|----------|
|
||||
| **589558**(新增) | 团期订单请通过团期预支入口申请 | 对**团期子订单**(挂在团期下的订单)调用订单级创建预支接口 `POST /v3/admin/order/{orderId}/advance` 时直接返回,不创建预支 |
|
||||
| 585003 | 当前订单状态不允许预支 | 订单状态不在 {待出发, 行程中} 或已结算完成(既有行为不变;团单判断优先于此码) |
|
||||
| 585004 | 预支金额超过可用余额上限 | 申请金额 > 可支取余额上限(既有行为不变) |
|
||||
|
||||
> 团单判断**先于**状态判断:团期子订单即使状态满足,也固定返回 589558,不会返回 585003。
|
||||
|
||||
## 8. 示例(3 组:典型 / 边界 / 异常)
|
||||
|
||||
### 8.1 业务失败(新增场景):团期子订单调订单级预支入口
|
||||
|
||||
**场景说明**:订单是团期产品的子订单,前端仍调了订单级入口 → 返回 589558。
|
||||
|
||||
**请求**:
|
||||
```
|
||||
POST /v3/admin/order/1234567890123456789/advance
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
```json
|
||||
{
|
||||
"payeeStaffId": 9876543210987654321,
|
||||
"advanceType": "TRAVEL_EXPENSE",
|
||||
"amount": 500.00,
|
||||
"purpose": "门票垫付"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 589558,
|
||||
"message": "团期订单请通过团期预支入口申请",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 典型成功:普通散客订单正常创建预支
|
||||
|
||||
**场景说明**:订单不挂在任何团期下,状态为待出发/行程中、未结算完成 → 行为与改前完全一致。
|
||||
|
||||
**请求**:
|
||||
```
|
||||
POST /v3/admin/order/1111222233334444555/advance
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
```json
|
||||
{
|
||||
"payeeStaffId": 9876543210987654321,
|
||||
"advanceType": "TRAVEL_EXPENSE",
|
||||
"amount": 500.00,
|
||||
"purpose": "门票垫付"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": "7777888899990000111",
|
||||
"orderId": "1111222233334444555",
|
||||
"teamNo": "26-0480",
|
||||
"payeeStaffId": "9876543210987654321",
|
||||
"payeeName": "张领队",
|
||||
"payeeRole": "GUIDE",
|
||||
"payeeRoleText": "导游",
|
||||
"advanceType": "TRAVEL_EXPENSE",
|
||||
"amount": 500.00,
|
||||
"purpose": "门票垫付",
|
||||
"voucherUrl": null,
|
||||
"status": "SUBMITTED",
|
||||
"statusText": "待审批",
|
||||
"rejectReason": null,
|
||||
"createdByName": "李财务",
|
||||
"createTime": "2026-09-28T16:00:00",
|
||||
"submittedAt": "2026-09-28T16:00:00",
|
||||
"approvedAt": null,
|
||||
"approvedBy": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 边界情况:团期财务 Tab 可支取余额口径
|
||||
|
||||
**场景说明**:某团期尾款池(unpaidAmount)= 7300.00;本团期有 1 笔预支已审批通过并已付款(PAID,金额 2000.00),核单尚未扣回。
|
||||
|
||||
**请求**:
|
||||
```
|
||||
GET /v3/admin/order/group-batch/5555666677778888999/finance
|
||||
Authorization: Bearer <token>
|
||||
(无请求体)
|
||||
```
|
||||
|
||||
**响应(改后)**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"receivableAmount": "40200.00",
|
||||
"receivedAmount": "32900.00",
|
||||
"unpaidAmount": "7300.00",
|
||||
"advanceApproved": "0.00",
|
||||
"advancePending": "0.00",
|
||||
"advanceAvailable": "5300.00",
|
||||
"items": [],
|
||||
"totals": { "totalPrice": "40200.00", "paidAmount": "32900.00", "unpaidAmount": "7300.00" },
|
||||
"withdrawnCount": 0,
|
||||
"primaryPayeeName": "张领队",
|
||||
"secondaryPayeeName": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 对比:改前同一数据下 `advanceAvailable` 返回 `"7300.00"`(PAID 的 2000.00 未计入扣减);改后返回 `"5300.00"`。该笔 PAID 预支在核单完成扣回后才会释放额度。
|
||||
|
||||
## 9. 业务边界
|
||||
|
||||
- 适用场景(订单级预支):不挂在任何团期下的普通散客订单,状态为待出发/行程中、未结算完成 → 行为与改前**完全一致**
|
||||
- 不适用场景(订单级预支):团期产品的子订单 → 固定返回 589558,须改用团期级预支入口(`POST /v3/admin/order/group-batch/{groupBatchId}/advance`,该接口逻辑本次不变)
|
||||
- 特殊边界(财务 Tab):团期下存在 PAID(已付款未核单)预支时,`advanceAvailable` 比改前变小;核单完成后该笔释放,`advanceAvailable` 回升。这是口径修正,不是数据异常
|
||||
|
||||
## 10. 修改前后对比
|
||||
|
||||
### 10.1 字段级对比
|
||||
|
||||
无字段增删 / 改名 / 类型变化。
|
||||
|
||||
### 10.2 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期子订单调订单级预支入口 | 放行(只看本单状态/上限),可能与团期级重复支取同一笔尾款 | 直接返回 **589558**,不创建预支 |
|
||||
| 团期财务 Tab「可支取余额」扣减口径 | 只扣 SUBMITTED + APPROVED | 扣 SUBMITTED + APPROVED + **PAID**(已付款未核单也占额度) |
|
||||
| 普通散客订单订单级预支 | 原有校验链 | **不变** |
|
||||
| 团期级预支入口 | 原有逻辑 | **不变** |
|
||||
|
||||
## 11. 影响评估 / 回滚
|
||||
|
||||
### 11.1 影响评估
|
||||
|
||||
- **是否破坏向后兼容**:行为收紧型变更。① 团期子订单此前能调通的订单级预支入口现在固定失败(589558)——若前端在团期子订单详情页暴露了订单级预支入口,需要处理该错误码;② 财务 Tab `advanceAvailable` 数值在存在 PAID 预支时变小,属口径修正
|
||||
- **前端是否必须同步上线**:否。接口字段结构零变化;但建议尽快处理 589558 的提示与入口引导(把团期子订单的预支操作引导到团期级入口)
|
||||
- **影响已有数据**:无数据迁移;既有 PAID 预支自动按新口径计入扣减
|
||||
|
||||
### 11.2 回滚方案
|
||||
|
||||
- **回滚方式**:revert merge commit `7bbc98c335`,重新部署 hl-order-service-v3 即恢复原行为
|
||||
- **回滚后清理**:无需清理数据 / 缓存
|
||||
- **回滚耗时**:约等于一次常规服务部署
|
||||
|
||||
## 12. 注意事项
|
||||
|
||||
- 若前端此前对「团期子订单调订单级预支失败」有任何本地兜底 / workaround(如自行判断团单后隐藏入口但保留了调用),本次后端已硬拦,相关兜底不会再触发到创建成功分支
|
||||
- 财务 Tab 的 `advanceAvailable` 请继续直接读接口返回值,不要本地自算(本次口径调整只发生在服务端,本地自算会与卡片对不上)
|
||||
- 需要重启的服务:**hl-order-service-v3**(测试服部署走 Deploy Panel 操作)
|
||||
|
||||
## 13. 关联 / 联系人
|
||||
|
||||
### 13.1 链接
|
||||
|
||||
- **Issue**: [#8384](https://git.1814.love/wx/HL/issues/8384)
|
||||
- **PR**: [#8479](https://git.1814.love/wx/HL/pulls/8479)
|
||||
- **Merge commit**: [7bbc98c335](https://git.1814.love/wx/HL/commit/7bbc98c3359ef2fe31cd69e9c098a29d625caca4)
|
||||
|
||||
### 13.2 联系人
|
||||
|
||||
- **后端负责人**: @yst
|
||||
某些文件未显示,因为此 diff 中更改的文件太多 显示更多
在新工单中引用
屏蔽一个用户